라우터와 메소드 사용하기

 

npx nest를 입력했을 때 뜨는 이것을 기억하는가?

 

사실 이것은 nest를 사용하기 위한 훌륭한 키워드 모음집이라고 할 수 있다.

이곳에 있는 키워드만으로도 nestjs의 거의 모든 것을 생성할 수 있다.

 

위의 Commands 목록을 보면 generate가 있고 이것의 짧은 키워드는 g인 것을 알 수 있다.

generate 밑으로 표가 하나 있는데 이는 생성하고자 하는 대상과 그것의 짧은 키워드이다.

 

예를 들어 컨트롤러를 새로 생성하고자 한다면

npx nest g co

위 커멘드를 입력하면 된다.

이를 입력하면 컨트롤러의 이름을 입력해주라는 말이 뜬다.

 

이름을 movies로 해보자

 

 

movies 폴더가 새로 생기고 컨트롤러 파일이 생성된 것을 확인할 수 있다.

 

파일을 열어 내용을 확인해보면 이미 필요한 내용은 다 만들어져 있는 것을 볼 수 있다.

어찌보면 이게 nest의 매력이지 않을까 싶다

참고로 spec은 테스트 파일이기 때문에 없어도 무관하다.

 

컨트롤러 파일을 편집해서 간단한 URL을 만들어보자

새로 추가된 url

import에 Get을 추가하고 movies라는 경로에 newnewnew를 출력해 보았다.

 

 

Param 사용하기

자, 이제 라우터 기능 중에 하나인 파라미터값을 받아보자

 

movies의 하위 url이 만들어졌다.

이로써 movies 하위 url에 뭘 입력하든 another가 출력될 것이다.

자, 이제 파라미터 값을 써보자

 

URL에 있는 id 값을 받아오고 이것을 myId라고 명명했으며 이것의 자료 타입은 string이다.

그리고 그 값을 another 옆에 출력하도록 작성했다.

 

보시다시피 이제 파라미터 값을 받아올 수 있게 되었다.

 

 

 

이제 다른 메소드도 사용해보자

 

Post와 Patch와 Delete 메소드를 추가했다.

참고로 put 메소드는 해당 페이지 전체를 업데이트하고 patch 메소드는 해당 페이지 일부만을 업데이트 한다.

이를 확인하기 위해선 브라우저에선 불가능하고 대신 썬더클라이언트를 활용해보자

썬더 클라이언트는 VS-CODE의 확장 프로그램중 하나다.

썬더클라이언트에서 POST 메소드를 테스트해보았다.

보시다시피 잘 작동하는 것을 알 수 있다.

 

 

 

Body 사용하기

 

post 메소드를 사용할 때, body는 정말 자주 사용한다.

post 메소드에 body를 한번 사용해 보자

 

포스트의 함수에 Body를 추가해주면 최상단의 import에 Body가 자동으로 추가된다.

 

추가 후 Body를 사용한 모습

 

이번엔 Patch 메소드에서 param과 body를 같이 사용해보자

Patch메소드에 이것저것 추가해준 모습

보시다시피 잘 작동한다.

 

주의사항

express를 사용할때도 있었던 문제점인데

만약 위 코드에서 movie 하위 URL로 search를 만든다고 할 경우,

해당 코드가 아래에 위치해 있으면 이를 :id로 인식할 수 있다는 것이다.

바로 이런 경우다.

 

만약 브라우저에서 URL에 movie/search라고 입력하면

브라우저는 search를 id로 인식하여 첫 번째 코드를 실행한다는 문제점이다.

이런 문제점을 없애는 방법은 아주 간단한데 그냥 순서를 바꿔주면 된다.

바로 요렇게

 

Query사용하기

이번엔 query를 사용해 보자

사실 이것도 앞의 body랑 별 다를 것이 없다.

앞서 하던 body와 거의 차이가 없는 모습

 

정상적으로 작동하는 것을 알 수 있다.

 

 

 

클래스

클래스란 사용자가 직접 만드는 일종의 틀이라고 할 수 있다.

변수(필드)와 함수(메소드)를 하나의 단위로 결합해주는 역할을 한다.

 

상속, 다형성, 파생 클래스, 클래스의 특수화 메커니즘 등이 있다.

접근 한정자

public class AAAA
{
	public int x, y;

	public AAAA()
	{
		실행문 내용 생략
	}

}

위 코드에서, public이 접근 한정자에 해당하고

접근 한정자는 public, protected, internal, protected internal, private, private protected가 있다.

public은 밖에서 액세스가 가능한 것을 의미한다.

protected는 이 클래스와 이 클래스에서 파생된 클래스로만 액세스 할 수 있다.

internal은 현재 어셈블리로만 액세스가 제한된다.

protected internal는 포함하는 클래스와 포함하는 클래스에서 파생된 클래스와 어셈블리내의 클래스만 액세스가 된다.

private는 이 클래스에서만 접근이 가능하다

private protected는 포함하는 클래스와 동일 어셈블리 내의 포함하는 유형으로부터 파생된 클래스만 액세스가 된다.

 

...뭔가 엄청 복잡하고 많은데 사실 대부분은 잘 안쓰는 거고

public과 private를 가장 많이 사용하며 protected도 종종 사용된다.

 

아래 예시를 보자

class AA{
	int a;
	public int b, c;

	public void Print(){
		Console.WriteLine(a,b,c);
	}
}

class Program
{
	static void Main(string[] args){
		AA aa = new AA();
		//aa.a = 5; 는 사용불가
		aa.b = 10;
		aa.c = 20;
		aa.Print();
	}
}

AA 클래스에는 int가 각각 a, b, c가 선언되어 있다.

이때, a만 접근한정자가 없는데, 이 경우 private가 기본적으로 적용된다.

b와 c는 public으로 정의되어 있으며

Print함수 또한 public으로 정의되어 있다.

 

아래 Program 클래스를 보자

Main함수에서, 후술할 생성자를 통해 AA 클래스를 쓰겠다고 했으며 이를 aa라고 명명했다.

AA 클래스에는 변수 a, b, c와 Print 함수가 있는데

b와 c의 값은 여기서 정해주는 것이 가능하나 a는 정해줄 수 없다.

 

왜냐하면 private 인 값은 다른 클래스에서 조작할 수 없기 때문이다.

반면 public인 b와 c와 Print는 얼마든지 사용가능한 것을 볼 수 있다.

 

 

생성자와 소멸자

바로 위에서, Main함수에서 AA 클래스를 사용하기 위해 new 키워드를 쓴 것을 볼 수 있다.

이것이 바로 생성자로 객체를 생성할 때 사용한다.

 

즉, AA클래스를 새로 하나 복사를 해서 이 클래스에서 쓰겠다는 것이며 이 사본의 이름을 aa라고 지은 것이다.

 

그렇다면 소멸자는 뭘까

소멸자는 GC라고도 하며 ~키워드를 사용한다.

여기서 ~는 클래스 앞에 붙여서 사용한다.

만약 위의 AA클래스에 소멸자를 붙인다면 ~AA가 되는 것이다.

 

아래 예시를 보자

class AA{
	int a;
	float b;

	public AA(){
		a = 0;
		b = 0f;
	}

	public AA(int aa, float bb){
		a = aa;
		b = bb;
	}

	~AA(){
		Console.WriteLine("제거")
	}

}

class Program
{
	static void Main(string[] args){
		AA aa = new AA();
		AA aaaa = new AA(100, 0.5f);
	}
}

시작하기 전에, AA클래스 안에는 AA가 총 두 종류가 있는 것을 볼 수 있다.

하나는 파라미터를 2개 받는 것이고 나머지 하나는 파라미터를 안 받는 것이다.

 

Main 함수에서, 파라미터를 받는 함수와 받지 않는 함수를 각각 따로 호출하고 있다.

여기서 소멸자는 AA 클래스 내의 모든 호출이 끝나면 자동으로 소멸자가 호출한 횟수만큼 작동한다.

위 코드에서는 AA 클래스를 총 2번 호출했으므로 "제거"라는 문자열이 두 번 등장할 것이다.

 

 

주의사항

생성자와 소멸자의 이름은 클래스의 이름과 같다.

생성자와 소멸자는 리턴이 없다.

생성자는 접근한정자를 쓰고 클래스의 이름을 넣어주면 된다.(자료형을 넣지 않는다.)

소멸자에는 어떠한 파라미터도 넣어줄 수 없다.

클래스 안에 생성자가 하나라도 선언되어 있을 경우 기본으로 제공하는 선언자가 작동하지 않는다.

이게 무슨말이냐면 아래 코드를 보자

class AA{
	public AA(){
	}
	public AA(int aa, float bb){
	}
}

class Program
{
	static void Main(string[] args){
		AA aa = new AA();
		AA aaaa = new AA(100, 0.5f);
	}
}

이 코드에는 문제가 없다.

그러나 AA 클래스의 public AA를 없애버리면

Main 함수의 AA aa = new AA(); 가 동작하지 않는다.

때문에 클래스에 생성자를 넣어줄 때는

반드시 빈 생성자라도 추가를 해주어야 한다.

 

 

 

 

서버 실행하기

nestjs를 설치하면 많은 기본 파일이 생성되는데

여기서 package.json파일을 열어서 서버를 어떻게 실행하는지 확인해보자

package.json 파일의 일부분

보시다시피 이렇게 되어 있는 것을 알 수 있다.

npm run start:dev

package.json 파일을 참조하여, 위 명령어를 터미널로 입력해주면

localhost:3000

경로로 서버를 실행할 수 있게 된다.

서버를 실행한 모습, Hello World!가 출력된다.

 

기본 파일

src 폴더에 main.ts 파일이 있다.

 

main.ts

해당 파일의 내용으로 보시다시피 포트넘버가 기본적으로 3000번으로 되어 있는 것을 볼 수 있다.

그렇다면 Hello World!는 어디서 오는 걸까?

바로 AppModule이다.

app.module.ts는 main 파일과 같은 폴더에 위치하고 있다.

app.module.ts

여기서 @Module이 바로 데코레이터이다.

테코레이터는 클래스에 함수기능을 추가해주는 것으로 가장 아랫줄에 보면 AppModule 클래스를 익스포트 한다고 되어있지만 정작 그 안에는 아무 내용도 없는데, 여기서 데코레이터로 추가된 것들이 추가된 것이다.

 

즉, AppModule이라는 클래스에는 Module이라는 데코레이터가 추가되어 있는 것이다.

 

자, 그런데 데코레이터에 AppController와 AppService가 있는 것을 볼 수 있다.

 

app.controller.ts
app.service.ts

먼저 컨트롤러 파일을 보자,

컨트롤러에는 get 데코레이터가 있고 서비스 파일의 getHello()를 리턴하고 있으며

서비스에선 getHello()가 Hello World! 문자열을 리턴하고 있다.

 

즉 모듈 파일은 컨트롤러 파일을 호출했고

컨트롤러 파일은 서비스 파일을 호출했고

서비스 파일이 Hello World!를 반환한 것이다.

 

컨트롤러

컨트롤러는 라우터와 비슷한 역할을 한다.

컨트롤러 파일에 새로운 데코레이터를 추가해보자

@Get('/hello')
sayHello(): string {
	return 'myHello';
}

여기서 Get은 메소드를 의미한다

'/hello'는 경로를 의미하고

sayHello()는 사용할 함수를 말한다.

함수에서, 문자열 myHello를 반환하도록 했으니, hello 라는 경로에 들어가면 myHello라는 문자열이 출력된다.

 

hello 경로로 들어간 결과

 

 

주의사항

데코레이터는 반드시 사용하고자 하는 함수나 클래스랑 붙어있어야 한다.

즉, 아래와 같이 쓰면 안 된다.

@Get('/hello')

sayHello(): string {
	return 'hello';
}

 

만약 Post 메소드를 쓰고자 한다면?

컨트롤러 파일의 최상단에 위치한 import에 Post를 넣어주면 된다.

import { Controller, Get } from '@nestjs/common';

// 위 코드를 아래로 바꾸어준다.

import { Controller, Get, Post } from '@nestjs/common';

 

 

서비스

서비스는 일반적으로 함수를 가지는 파일이다.

위에서, 컨트롤러에서 myHello를 return하도록 했는데

이렇게 하지말고 서비스 파일에서 myHello를 출력하는 함수를 만들고

 

그 다음 컨트롤러에서 출력하게 해보자

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
  getHi(): string {
    return 'myHello';
  }
}

서비스 파일에 getHi 함수를 추가한 모습이다.

이제 컨트롤러 파일을 수정해주자

 

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Get('/hello')
  sayHello(): string {
    return this.appService.getHi();
  }
}

 

컨트롤러 파일의 sayHello 부분을 서비스에 있는 getHi 함수를 리턴해주는 형태로 바꾸어 주었다.

 

 

자 이제 Nodejs의 express에서 하던 방식을 온전히 nestjs의 방식으로 옮기는 가장 기본적인 부분을 살펴보았다.

 

사실 app.controller와 app.service는 뭔가 예시파일 같은 느낌이다.

고로 이걸 그냥 없애버리고 내가 직접 만들어보도록 하자

 

기타

nestjs에서는 Nodemon의 기능을 기본적으로 지원하는듯 하다.

실제로 코드를 편집하고 저장해보면 바로 재시작이 되는 것을 알 수 있다.

Nest.js란 무엇일까?

 

Nest.js란 Node.js에서 구동되는 프레임워크고 node.js에 백엔드를 구성할 수 있게 해준다.

express를 이용하는 백엔드 개발을 위한 프레임 워크로써

특별한 구조가 정해져있고 이를 따르기만 하면 비교적 큰 규모의 백엔드를 비교적 쉽게 만들 수 있게 된다.

TypeScript를 사용하기 때문에 이를 배우기 전에 먼저 TypeScript부터 제대로 익히는 것이 좋다.

 

Nest.js는 Node.js에서 구동되는 프레임워크이므로 우선 node가 있어야 한다.

node의 설치과정은 생략한다.

 

시작하기

VS-Code에서 터미널을 열고 먼저 nestjs를 설치하도록 하자

npm i -g @nestjs/cli

설치하는데 약간의 시간이 걸린다.

 

 

npx nest

설치가 완료되면 위 명령어를 입력하면 아래 사진 처럼 나오는 것을 볼 수 있다.

이제 새로운 nest 프로젝트를 만들어야 한다.

터미널에 다음을 입력한다.

npx nest new

그러면 터미널에서, 새로운 프로젝트의 이름은 무엇인가 라고 물어볼 것이다.

원하는걸로 이름을 정해주자

 

이름을 정했으면 패키지 메니저로 무엇을 사용할 것인지를 묻는데 npm, yarn, pnpm 중에서 선택해야 한다.

나는 노드를 사용하므로 npm을 사용하겠다.

 

여기까지 왔으면 설치가 시작되는데 이게 시간을 좀 먹는다.

커피나 마시면서 여유를 가지고 기다려보자

 

다 설치되었으면 이렇게 틀이 만들어진 것을 볼 수 있다.

 

이제 본격적으로 시작해보자

배열을 파라미터로 사용하기

배열을 파라미터로 사용할 수도 있다.

static void AAA(int a, int b, int[] array){
	int temp = array[a];
	array[a] = array[b];
	array[b] = temp;
}

static void Main(string[] args){
	int[] arrNum = new int[]{1, 2, 3, 4};
	AAA(0, 1, arrNum);
    
	foreach(int a in arrNum)
		Console.WriteLine(a);
}

위 코드는 1,2,3,4가 들어있는 배열에서 0번 인덱스의 값과 1번 인덱스의 값을 서로 바꿔주는 코드다.

맨 아래 foreach문은 그렇게 바뀐 값을 출력하기 위한 코드다.

 

출력하면 2,1,3,4가 출력되는 것을 알 수 있다.

 

배열이 리턴이 되는 경우

static int[] AAAA(int a){
	int[] ARRAY = new int[a];
	for(int i = 0; i < ARRAY.Length; i++){
		ARRAY[i] = 0;
	}
	return ARRAY;
}

위 함수는 a를 파라미터로 받는다.

그리고 그 파라미터 숫자만큼의 크기를 가진 배열을 생성하고

그 배열에는 모두 0이라는 값을 넣는다.

 

Clear, Length, GetLength, Clone

clear는 배열을 초기화 하는 명령이다.

int[] a = new int[3];

Array.Clear(a, 0, a.Length);

위 코드는 길이가 3인 배열을 선언하고

그 배열을 0번 인덱스 부터 배열의 마지막 인덱스 까지 초기화 하는 코드다.

 

Length는 배열의 길이를 가져오는 명령이다.

int[] a = new int[3];

Console.WriteLine(a.Length);		// 3

 

GetLength는 다차원 배열에서 행의 갯수나 열의 갯수를 가져오는 명령이다.

int[,] a = new int[2,3] {{1,2,3},{4,5,6}};

a.GetLength(0)		// 행의 갯수는 2개
a.GetLengrh(1)		// 열의 갯수는 3개

 

Clone은 복사를 할 때 사용한다.

 

 

 

 

일차원 배열

일차원 배열은 다음과 같은 형태로 선언한다.

int[] arrNum = new int[5]

이것은 정수 int의 배열로 배열의 이름은 arrNum이다.

총 5칸의 배열이 있다.

 

배열 선언과 초기화 법은 다음과 같다.

int[] arrNum = new int[5];
int[] arrNum = new int[]{1, 3, 4, 7, 2};
int[] arrNum = new int[5]{1, 3, 4, 7, 2};
int[] arrNum = {1, 3, 4, 7, 2};

가장 첫 번째 줄을 제외하고,

그 아래 3개는 모두 1,3,4,7,2 가 대입된 5칸 짜리 배열을 선언하는 내용이다.

같은 내용이지만 이렇게 다양한 방법으로 작성해 줄 수 있다.

 

배열에는 인덱스라는 개념이 있다.

이는 배열내용의 몇 번째에 해당하는 숫자인지를 나타낸다.

예를 들어, 위에 선언한 1, 3, 4, 7, 2 로 구성된 배열의 0번째 인덱스의 값은 11

1번 인덱스 값은 3

2번 인덱스 값은 4

3번 인덱스 값은 7 이다.

가장 첫 번째 칸은 0번 인덱스라는 것에 주의할 것

 

 

foreach - 반복문

foreach는 읽기 전용이며 배열에 사용하는 반복문이다.

int[] arrNum = {1, 3, 4, 7, 2};

foreach(int data in arrNum){
	Console.Write(data);		//결과 값은 1 3 4 7 2
}

arrNum이라는 배열에서 0번째 인덱스 부터 하나씩 꺼내서 int data에 넣는다.

이를 Console.Write를 통해 출력하므로, 결국 arrNum의 값이 0번 부터 순차적으로 나타나는 결과가 나온다.

 

다차원 배열

1차원 배열이 단순히 나열이라는 느낌이라면 2차원 배열은 표와 같고 3차원 배열은 블록 쌓기 같은 느낌이다.

  0 1
0 [0,0] [0,1]
1 [1,0] [1,1]
2 [2,0] [2,1]

위 표는 2차원 배열의 예시를 나타낸 표다.

다차원 배열은 2차원이나 3차원까지는 드물게나마 사용하지만 4차원부터는 거의 안쓴다고 보면 된다.

2차원 배열은 그래도 종종 사용하니까 알아두자

 

보통 2차원 배열은 과거 RTS 게임에서 타일의 위치를 나타낼 때 사용하곤 했다.

 

이제 2차원 배열을  선언해 보자

int[,] arrNum = new int[3,2];
int[,] arrNum = new int[,]{{0,1}, {2,3}, {4,5}};
int[,] arrNum = new int[3,2]{{0,1}, {2,3}, {4,5}};
int[,] arrNum = {{0,1}, {2,3}, {4,5}};

1차원 배열과 마찬가지로, 여러가지 방법이 있다.

첫 번째 줄만 제외하고, 나머지 3개는 다 같은 배열을 선언하는 내용이다.

 

이번엔 3차원 배열을 선언해 보자

int[,,] arrNum = new int[4,3,2];
int[,,] arrNum = new int[,,]{
    {{0,1}, {2,3}, {4,5}},
    {{2,1}, {3,3}, {1,5}},
    {{3,1}, {6,3}, {8,5}},
    {{4,1}, {5,3}, {9,5}}
};

이쯤되면 알겠지만, 대괄호 안의 쉼표로 이게 몇 차원 배열인지를 나타낸다.

 

다차원 배열은 깊게 파고들 수록 어렵기보다 복잡함이 더한데

그래봐야 별로 쓸 일은 없는 고로 여기까지만 알아보고 넘어가자

 

가변 배열

배열의 크기가 가변적인 배일이다.

int[][] arrNum = new int[3][];

위 코드를 보면 행은 3개로 정해져 있지만 열의 수는 정해져 있지 않다.

 

이러한 가변 배열을 이해하기 쉽게 예시와 표로 표시하면 다음과 같다.

int[][] arrNum = new int[3][];

arrNum[0] = new int[2] {0, 1};
arrNum[1] = new int[4] {0, 1, 2, 3};
arrNum[2] = new int[] {0, 1, 2};
  0 1 2 3
0 0 1    
1 0 1 2 3
2 0 1 2  

일반적인 2차원 배열은 그 형태가 반드시 직사각형 형태가 되지만

보시다시피 가변 배열은 직사각형 형태가 아니라 뭔가 살짝 일그러진 도형이 된다.

 

 

 

 

 

 

함수

 

이 사진을 보면 함수가 무엇인지 이해하기 쉬울 것이다.

 

다르게 설명하자면 내가 만든 일종의 공식이라고

할 수 있다.

 

예를 들어, 삼각형의 넓이를 구하는 공식은

(가로길이) X (세로길이) / 2

이다.

 

 

이 공식에 가로길이 값을 넣어주고

세로길이 값을 넣어주면 우리는 쉽게 삼각형의 넓이를 구할 수 있다.

 

함수가 바로 내가 임의로 만드는 공식과 같다고 할 수 있다.

 

먼저, 내가 입력받아야 하는 값(가로길이와 세로길이)이 바로 파라미터(매개변수)라 하고

계산 결과 나온 값을 리턴 값 (반환 값)이라고 한다.

 

위에 설명한 삼각형 넓이 구하는 공식을 함수로 만들어보면 다음과 같다.

static int AAA(int a, int b)
{
	int c = a * b / 2
	return c
}

여기서 int a와 int b가 내가 입력해야할 가로길이와 세로길이가 된다.

c는 넓이가 된다.

AAA는 내가 만든 함수의 이름이 된다.

 

그렇다면 이 함수를 써보자

static int AAA(int a, int b)
{
	int c = a * b / 2
	return c
}


int num1 = 10;
int num2 = 30;

int result = AAA(num1, num2)
Console.WriteLine(result)

이렇게 하면 함수 AAA에 각각 10과 30이라는 숫자가 들어가고

그 결과값이 result에 저장될 것이다.

 

모든 함수가 파라미터와 리턴이 존재하는 것은 아니다.

파라미터가 없는 함수도 있고 리턴이 없는 함수도 있고 둘 다 없는 함수도 있다.

 

리턴이 없는 함수는 void를 써주어야 한다.

 

 

Call by Value와 Call by Reference

Call by Value는 값에 의한 호출로 함수에서 값에 영향을 주지 않는다.

Call by Reference는 주소에 의한 호출로 함수에서 값에 영향을 준다.

 

주소에 의한 호출을 사용하고 싶다면 파라미터의 자료형 앞에 ref를 붙여주면 된다.

 

아래 예제를 참조하자

static public void ValueSwap(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;
}
static public void RefSwap(ref int a, ref int b)
{
	int temp = a;
	a = b;
	b = temp;
}
static void Main(string[] args)
{
	int num1 = 100;
 	int num2 = -100;

	ValueSwap(num1, num2);
	Console.WriteLine(num1, num2);

	RefSwap(ref num1, ref num2);
	Console.WriteLine(num1, num2);

}

메인함수를 포함하여, 총 3개의 함수가 있다.

맨 처음 ValueSwap는 값에 의한 호출로 파라미터 값을 서로 스위핑 해주고

RefSwap는 참조에 의한 호출로 파라미터 값을 서로 스위핑 해준다.

 

Main 함수를 보면 num1 은 100, num2는 -100로 되어있다

ValueSwap 함수 내에서, num1과 num2의 값은 서로 바뀌겠지만

그것은 어디까지나 함수 내에서만 발생하는 것이며, 함수 밖의 값은 전혀 영향이 가지 않는다.

 

그러나 RefSwap 함수 내에서, num1 값과 num2 값은 서로 바뀌고 그것이 함수 밖의 원본에까지

영향을 끼친다.

 

out

out은 ref와 사용방법이 같은 키워드로 전달하는 변수 초기화 없이 사용 가능하다

보통 out을 쓰는 경우, 리턴이 없는 경우가 꽤 있다.

 

static void InitNum(out int addNum)
{
	addNum = 100;
}
static void InitRefNum(ref int addNum)
{
	refNum = 100;
}
static void Main(string[] args)
{
	int a;
	int b;
 
 	InitNum(out a);
 	Console.WriteLine(a)		// 출력 결과는 100
 
 	InitRefNum(ref b);		// 에러!! b를 초기화 하지 않고 사용할 수 없음!!
 	Console.WriteLine(b)
}

메인함수의 InitNum을 보자, 초기화가 안 된 변수 a가 out 키워드와 함께 파라미터로 들어갔다.

그리고 그렇게 들어간 파라미터는 주소에 의한 호출값이므로

원본 값 a의 값을 변화시킨다.

 

반면 InitRefNum 같은 경우, 위 코드대로 하면 에러가 발생한다.

b값이 초기화 되지 않았기 때문에 사용할 수 없기 때문이다.

 

즉 ref과 out의 차이는 초기화가 필요한가 아닌가로 나눌 수 있을 것이다.

 

파라미터 초기화

파라미터는 뒤에서 순서대로 초기화 된다.

static void AAAA(int a, int b, int c = 100, int d = 0)
{
	Console.WriteLine(a, b, c, d);
}

static void Main(string[] args)
{
	AAAA(0, 0, 0, 0);
}

AAAA라는 함수를 정의했고, Main함수에서, AAAA함수에 0, 0, 0, 0을 넣었다.

이 경우, AAAA 함수 내에서, a, b, c, d의 값은 전부 0을 받고 Console.WriteLine을 통해, 0 0 0 0이 출력된다.

 

그렇다면 파라미터에서 int c = 100은 무슨 역할을 하느냐

만약 AAAA(10, 2) 를 Main 함수에 넣을 경우, int c와 int d는 파라미터를 받지 못하게 된다.

이럴 경우, 기본 값으로써 c는 100, d는 0을 받게 되는 것이다.

 

파라미터는 반드시 뒤에서 순서대로 초기화 된다.

때문에 아래 구문처럼 작성할 수 없다.

static void AAAA(int a, int b = 50, int c, int d = 0)
{
	Console.WriteLine(a, b, c, d);
}

c는 기본 값이 주어지지 않았는데, b에 기본값을 넣어줄 수 없다.

즉 기본값은 반드시 뒤에서부터 순서대로 와야 한다.

 

메소드 오버로딩

메소드 오버로딩이란 메소드 이름이 중복되는 것을 말한다.

C#에서는 함수의 이름을 동일하게 사용할 수 있다.

물론 이 경우, 엄청난 에러가 발생할 수 있지만, 나름대로 구분법이 있으니

이름은 같아도, 파라미터의 수나 타입이 다르면 별개의 함수로 인식한다.

아래 예시를 보자

static int AAAA(int a, int b)
static int AAAA(int a, int b, int c)
static int AAAA(float a, float b)

AAAA라는 이름의 함수가 3개나 선언되었지만 파라미터가 전부 따로 노는 만큼 각자 다른 함수로 인식한다.

이것이 바로 오버로딩이다.

 

 

그러나 이는 다음과 같은 상황에서 심각한 부작용을 초래할 수 있다.

static void AAAA(int a, int b, int c)
{
	return (a + b + c + d)
}
static void AAAA(int a, int b, int c, int d = 23)
{
	return (a + b + c + d) * 2
}

static void Main(string[] args)
{
	Console.WriteLine(AAAA(11,22,33))
}

자, AAAA 함수가 두 개가 선언되어 있다.

두번째 AAAA함수의 파라미터 d는 디폴트 값 23 을 가진다.

 

Main함수에서, AAAA함수에 파라미터를 3개를 넣었다.

 

이때, 과연 어떤 함수가 실행될지 예상할 수 있는가?

당연히 모호성으로 인한 오류가 발생하게 된다.

때문에 위 코드는 아래와 같이 수정해줄 필요가 있다.

static void AAAA(int a, int b, int c)
{
	return (a + b + c + d)
}
static void AAAA(int a, int b, int c, int d)		//기본값을 제거
{
	return (a + b + c + d) * 2
}

static void Main(string[] args)
{
	Console.WriteLine(AAAA(11,22,33))
}

두 번째 AAAA 함수에서 기본값을 제거하였으니

Main함수의 AAAA 함수는 첫 번째 것이 실행된다.

 

params

파라미터를 무한정 받고 싶을 때 사용하는 키워드다.

아래 예시를 참조하자

static void AAAA(params int[] a)
{
	return (a.Length)
}

static void Main(string[] args)
{
	Console.WriteLine(AAAA(11,22,33,44))
}

위 코드에서, 함수 AAAA는 정수형 파라미터를 무한정 받는다.

이때, 파라미터는 배열의 형태로 받게되고 이 배열의 이름은 a다.

return에서는 배열 a의 길이를 리턴하라고 되어 있는데

이는 곳 a 가 받은 파라미터의 수를 뜻한다.

 

Main 함수를 보면 AAAA함수에 파라미터를 총 4개 넣은 것을 알 수 있다.

물론 4개가 아니라 더 적게, 더 많이 넣어줄 수도 있다.

 

 

 

 

 

 

지역변수

선언 된 변수는 그 {} 안에서만 유효하다는 특징이 있다.

int a;
{
	int b;
	int a;		// 이건 애러!!
}
{
	int b;
}

b = 10;			// 이것도 애러

위 식을 보자

맨 처음 int a가 선언되었다.

그리고 괄호 안의 int a는 사용이 불가능 한데 이미 사용중인 이름이기 때문이다.

반면 아래 따로 격리된 괄호 안에서는 b를 사용할 수 있다.

 

그리고 괄호 밖에 벗어나있는 b는 애러라고 뜬다.

위의 두 int b는 어디까지나 그 괄호 안에서만 유효하기 때문에

가장 밖의 공간에서는 int b가 선언된 적이 없으므로 사용이 불가능 한 것이다.

 

다른 괄호 안에 있기 때문인데 이를 지역변수라고 한다.

 

분기문(branch)

if 문(조건문)

if문은 가장 중요하면서도 자주 사용하는 분기문이다.

C# 뿐만 아니라 다른 언어에서도 흔하며 어디서나 자주 쓰기 때문에 이건 꼭 알고 넘어갈 필요가 있다.

if (조건식) {
	실행문
}

형태는 위와 같다.

조건식이 true이면 실행문을 실행하고 false면 실행하지 않는다.

if (조건식){
	실행문
}else {
	실행문
}

비슷한 형태의 if ~ else 문이다.

만약 조건식이 false라면 else에 있는 실행문을 실행하는 것이다.

 

if (조건식){
	실행문 1
}else if (조건식){
	실행문 2
}else {
	실행문 3
}

또 비슷한 형태의 if ~ else if ~ else 문이다.

만약 첫번째 조건식이 false라면 그 아래 else if의 조건식을 본다.

그 조건식이 true라면 실행문 2를 실행하고

만약 false라면 실행문 3을 실행한다.

 

else if는 계속 줄줄이 달아줄 수 있다.

bool a = true;

if (a){
	Console.WriteLine("참이다.")
}else {
	Console.WriteLine("거짓이다.")
}

조건식 안에 a가 들어가 있는데, 이 a는 true다.

때문에 '참이다.'가  출력되고 '거짓이다.' 는 출력되지 않는다.

 

int a = 12;
int b = 8;

if (a > 10 && b < 10){
	Console.WriteLine("참이다.")
}else {
	Console.WriteLine("거짓이다.")
}

두번째 예시문이다.

if문의 조건문을 보면 a는 10보다 크기 때문에 참이다.

그리고 b는 10보다 작기 때문에 참이다.

그리고 이 두 개의 식이 둘 다 참이므로

if문의 조건식은 참이된다.

 

고로 '참이다.'가 출력된다.

 

switch문

체감상 그렇게 자주 쓰는것 같지는 않지만

만약 조건식이 여러개 있을 경우, switch문을 쓰기도 한다.

형태는 아래와 같다.

switch(조건식)
{
	case 조건:
	실행문 1
	break;
    
	case 조건:
	실행문 2
	break;

	case 조건:
	실행문 3
	break;
    
	....
}

보시다시피 case는 얼마든지 붙을 수 있고

case가 끝날 때는 반드시 break를 넣어줘야 한다.

 

예시를 만들어보자

int num = 10;

switch(num / 10)
{
	case 10:
	case 9:
		Console.WriteLine("잘했어")
		break;

	case 8:
		Console.WriteLine("8점")
		break;
	case 7:
		Console.WriteLine("7점")
		break;
	default:
		Console.WriteLine("몰라")
		break;
}

num은 10이고 num을 10으로 나누었을 때,

그 값이 10이거나 9면 "잘했어"를 출력하고

8이면 "8점", 7이면 "7점"을 출력하라는 알고리즘이다.

 

맨 위 case 10을 보면 아무것도 없고 바로 case 9로 넘어가는데

이는 만약 (num / 10)의 값이 10이거나 9이면 Console.WriteLine("잘했어")를 실행하라는 의미다.

 

맨 아래 default는 위의 조건들 중 어느것도 해당하는게 없을 경우

실행하는 부분이다.

 

반복문

반복문에는 for문, while문, do~while문이 있으며 기본적인 형태는 아래와 같다.

 

for(초기식, 조건식, 증감식){
	실행문
}



while(조건식){
	실행문
}



do{
	실행문
}while(조건식);

while문은 조건식 안의 내용이 참이면 실행문에 적힌 내용을 계속 반복하는 문이다.

 

while과 do~while은 사실상 거의 같으나 한 가지 차이가 있다면

do ~ while은 조건과 상관없이 무조건 한 번은 실행한다는 차이점이 있다.

while은 시작하기전에, 조건이 안맞으면 아예 실행 자체를 안한다.

 

for문은 초기식과 증감식이 있는데

이는 반복 조건용으로 사용한다고 보면 된다.

for (int i = 1; i < 10; i++){
	실행문
}

보통 위와 같이 사용한다.

i라는 변수가 초깃값 1로 선언이 되고

i가 10보다 작으면 실행문을 실행한다.

그리고 실행문을 한 차례 다 실행했으면

증감문의 i++를 따라, i 값이 1 증가한다.

 

즉, 반복할 수록 i 값이 1씩 계속 증가하게 되는데, 어느 순간 i < 10을 만족하지

않는 순간이 오고 이 때가 되면 반복을 중단한다고 생각하면 된다.

 

for문은 중첩이 가능하다.

그러나 그다지 좋은 방법은 아니기 때문에 너무 많이 중첩하는 것은 지양하는 것이 좋다.

for (int i = 1; i < 10; i++){
	실행문 1

	for (int j = 1; j < 10; j++){
		실행문2
	}
    
	실행문 3
}

중첩된 for문의 예시, 2~3번 중첩 정도는 할 수 있지만, 너무 많이 중첩하면 그만큼

최적화 효율이 떨어지니 지양할 것

 

무한 반복

while과 for문을 적당히 응용하면 무한 반복을 만들 수 있다.

while과 for문으로 무한 반복을 만드는 방법은 다음과 같다.

while(true){
	실행문
}



for(;;){
	실행문
}

while의 경우, 조건문에다가 그냥 true를 넣어버리면 말 그대로 무한히 반복하게 된다.

for문의 경우, 초기문, 조건문, 증감문을 그냥 빈값으로 넣어버리면 된다.

 

이러한 무한 반복은 왜쓸까 궁금할 수 도 있는데

저렇게 세팅했다고 무조건 무한반복을 하는 것은 아니고

무한 반복문을 중간에 탈출 할 수 있도록 코드를 구성하면 된다.

 

즉, 정확히 얼마나 반복해야하는지 알 수 없을 때 사용할 수 있다고 보면 된다.

 

이것에 대해선 다음 문단에서 다루겠다.

 

점프문(break, continue, goto)

break문은 가장 중요하고 또 자주사용될 문으로 반복문을 완전히 끝내는 역할을 한다.

continue는 이하의 내역을 스킵하고 다음 반복으로 넘어가는 역할을 하며

goto는 반복문 내에서 특정 명령을 수행하는 역할을 한다.

 

아래 break 예제를 보자

int a = 1;

while(true){
	a++;
	if (a > 10){
		break;
	}
}

a는 1로 시작하고 무한반복문 내에서 a값은 1씩 무한히 증가한다.

그러다가 a가 10을 넘어가는 순간 break가 실행되면서

반복문이 종료되게 된다.

 

이번엔 continue 예제를 보자

 

int a = 1;
int b = 1;
while(true){
	a++;
	if (a == 5){
		continue;
	}
	b = a + b
}

a는 1씩 무한히 증가하고 b는 a값과 b 값을 더한만큼 증가한다.

그런데 a가 5가 될 때, continue가 실행된다.

이 경우, a가 5가 되었을 때, 바로 밑의 b = a + b가 실행되지 않고

다시 반복문의 첫 부분으로 돌아간다.

즉,

a + b는 아래 순서대로 진행이 된다.

2 + 1

3 + 3

4 + 6

6 + 10

7 + 16......

왼쪽 항은 1씩 증가하는데 여기서 5는 continue에 의해 스킵된 것이다.

 

이번엔 goto 예제를 보자

for(int i = 0; i < 10 ; i++){
	if(i==5)
	{
		goto AA;
	}
	if(i==7)
	{
		goto BB;
	}
}
AA:
	Console.WriteLine("AA를 실행")
BB:
	Console.WriteLine("BB를 실행")

i가 0에서 시작해, 1씩 증가하여 10이 될 때까지 반복하는 for문이다.

 

여기서 i가 5가 되는 순간, 반복문 밖의 AA를 실행하며

i가 7이 되는 순간, 반복문 밖의 BB를 실행하게 된다.

 

 

+ Recent posts