프로그래밍 언어의 단어와 문법 - 2 [프로그래밍]
안녕하세요. ST_ 입니다.
저번에는 프로그래밍에 대한 간단한 정의와 수식, 키워드에 대해 알아보았습니다.
'절차'를 설명해보도록 하죠. 절차는 기본적으로 읽는 순서를 의미합니다. 대부분 위에서 아래로 읽는 것은 알지만 한 줄에서 나열되었을 경우 어디서부터 시작하는지 헷갈려 하는 분들도 꽤나 많은 것 같습니다.
특히 C언어를 배우다가 Java나 C#, Swift를 배우기 시작했을 때 많이 생소하게 느껴지기도 하죠. C언어 같은 경우는 한 줄에 한 동작을 실행하도록 코딩하며 그렇게 코딩하도록 권장하고 있는 반면에 위의 언어들은 여러 동작을 하나로 묶을 수 있죠. 이는 객체지향 언어에서 두드러지게 나타나는 특징입니다.
절차에서는 크게 두 가지가 있는데, '키워드 나열'과 '함수(메소드) 연결'이 있습니다. 각각 예를 들어 보겠습니다.
[문법편 - 절차 (키워드 나열)]
먼저 키워드 나열입니다. 자바(Java)를 접해보신 분들은 이 문구를 지겹도록 봤을 겁니다.
public static void main(String[] args)
하지만 이 문구가 왜 이런지는 처음에 잘 알려주지 않죠. 이번 기회에 한 번 알아보고 가봅시다. 아까 말했듯이 '절차' 즉, 순서대로 진행하여 읽으면 이해가 빠를 겁니다.
public이라는 단어는 말 그대로 공용입니다. 약간 어렵게 말하면 접근제어자 중 하나로 어디서나 접근 가능하다는 의미를 갖고 있죠. 한마디로 누구나(어디서나) 쓸 수 있다는 의미죠. main, 그러니까 실행의 중심이 되는 부분은 어디서나 접근해야 쓸 수 있어야 하겠죠?
static은 정적이라는 의미입니다. static의 경우 해당 키워드를 쓰는 변수나 메소드의 경우 자바가 컴파일 함과 동시에 선언이 됩니다. 메인 메소드같이 프로그램의 시작점인 경우 객체를 생성하지 않고도 작업을 수행해야 하기 때문에 반드시 static을 붙여주어야 하는 것이죠. static 키워드의 경우 조금은 어려운 개념이라 대강 선언을 하면 프로그램의 시작부터 끝까지 메모리에 할당되어있게 하는 것이라고 생각하면 될 것 같습니다.
void는 기타 다른 언어와 마찬가지로 return(반환) 값이 없다는 의미입니다. main메소드의 끝에 도달하면 프로그램의 종료를 의미하기 때문에 무언가를 반환할 필요가 없죠. 그렇기 때문에 void가 붙는 것입니다.
main()은 많은 언어들과 마찬가지로 프로그램의 시작이 되는 메소드입니다. 프로그램이 시작되면 가장 먼저 실행된다고 생각하시면 됩니다.
String[] args는 매개변수를 보내서 실행할 때 사용됩니다. 아마 자바를 처음 접하시는 분들이라면 이 변수는 크게 써 볼일은 없을 겁니다. 여하튼 매개변수를 보낼 때 변수가 몇 개가 될지 모르기에 String타입의 배열로 받는 겁니다. args는 꼭 args가 아니어도 됩니다. 만약 실행 전에 매개변수를 보내고 그 변수들을 사용할 경우에 args[0], args[1] 이런 식으로 쓸 수 있죠.
키워드의 나열은 순서에 영향을 크게 받지 않습니다. 다만 메소드(함수)의 경우는 메소드명 바로 이전에 반환타입을 적어주어야 합니다.
일단 보편적으로 아래와 같은 순서로 씁니다.
접근제어자 - 데이터 상태 - 타입
예로들어 public static void, private static int, public final int, public final static char, 등등.. 이런 순서가 가장 보편적입니다.
정리하자면 "공용이면서 정적이고 반환 값이 없는 메인이라는 이름의 메소드다"라는 정도의 의미로만 받아들이시면 될 것 같습니다.
[문법편 - 절차 (객체 및 메소드 연결)]
두 번째는 객체 또는 메소드(함수) 연결입니다. 대개 많은 객체지향 언어들은 여러 기능들을 묶거나 접근할 때 이를 연결시켜주는 기능이 있습니다.
(C언어, 베이직 같은 절차지향언어만 배웠다면 조금 생소할 수는 있습니다만, 대부분의 언어들이 객체지향적이기도 하고 트렌드 자체 또한 마찬가지이기 때문에 알아두시는 것이 좋을겁니다.)
일단 예제를 보면서 설명을 하도록 하죠. 보통 객체지향 언어를 배우신 분들이라면 이런 것을 많이 봤을 겁니다.
name_A.b_method();
name_A.value;
이런 형식들을 많이 볼 수 있는데 위와 같이 객체에 접근하고자 할 때 객체 이름 다음 마침표(.)를 쓴 뒤 해당 객체의 필드(변수)라던가 메소드를 써주게 되죠. name_A.b_method(); 를 해석하면 name_A라는 객체의 b_method() 메소드(함수)를 호출(접근)한다. 라는 의미입니다. 두 번째의 name_A.value; 의 경우는 name_A 객체의 value라는 필드(변수)를 호출(접근)한다는 의미죠. 이렇게 어떠한 객체에 특정 기능 메소드(함수) 또는 변수(필드)를 사용하고자 할 때 객체에 연결해주는 기능을 하는 것이 바로 마침표(.)의 기능 중 하나입니다.
정리하자면 '객체의 필드 또는 메소드 호출'이라고 보시면 됩니다.
그럼 다른 하나의 기능은 무엇일까요? 앞서 말한 객체의 접근이라는 것을 좀 더 확장해서 생각하면 됩니다. 대부분의 많은 언어들은 객체로 다룬다고 했죠. 예로들어 한 객체의 임의의 메소드(함수)를 호출한다면 반환되는 값이 있을겁니다. 그 반환하는 데이터의 타입 또한 객체죠. 그렇다면 반환되는 객체에 대해 또 메소드 호출을 할 수 있을까요? 그리고서 반환 된 객체에 대해 또다시 메소드 호출을 할 수 있을까요? 대답은 "예'입니다.
무슨 말인지 모를 수 있습니다. 일단 천천히 예제를 보면서 하나씩 이해 해보도록 하죠.
<풀어쓴 코드>
Integer a1 = Integer.valueOf("12345");
String a2 = a1.toString();
char[] a3 = a2.toCharArray();
/*
Intege.valueOf(String s)
-> 문자를 숫자로 변환하여 Integer 타입을 반환합니다.
Integer.toString()
-> Integer 객체에 저장된 값을 문자열인 String타입으로 반환합니다.
String.toCharArray()
-> String타입의 문자열을 각 char타입의 문자로 나눈 배열로 반환합니다.
*/
<압축한 코드>
char[] a = Integer.valueOf("12345").toString().toCharArray();
/*
Intege.valueOf(String s)
-> 문자를 숫자로 변환하여 Integer 타입을 반환합니다.
Integer.toString()
-> Integer 객체에 저장된 값을 문자열인 String타입으로 반환합니다.
String.toCharArray()
-> String타입의 문자열을 각 char타입의 문자로 나눈 배열로 반환합니다.
*/
위에서 주석으로 각각의 기능은 말해드렸으니 연결과정을 잠깐 살펴봅시다.
Integer 라는 클래스에는 여러 메소드들이 있습니다. 여기서 쓰이는 메소드들은 Integer.valueOf()와 Integer.toString()입니다. 그리고, String 클래스에는 toCharArray()라는 메소드가 있죠. 이렇게 서로 다른 객체지만 반환 타입을 잘 활용한다면 위의 압축한 코드처럼 사용할 수 있습니다.
먼저 Integer.valueOf() 의 경우 "12345"라는 문자열을 숫자 12345로 바꾸어 Integer 타입으로 반환합니다.
그럼 Integer 타입으로 반환 된 객체에 toString()이라는 메소드를 사용하여 String 타입으로 반환합니다.
String 타입으로 반환된 객체에 있는 toCharArray()라는 메소드를 사용하여 char[] 배열로 반환합니다.
반환된 값은 char[] 타입의 a라는 변수에 저장됩니다.
이렇게 쉽게 객체타입에 있는 메소드를 호출 할 수도 있습니다.
R 언어를 배우신 분들은 %>% 라는 파이프 연산자를 접해보셨을 겁니다. 이와 비슷한 역할을 해주는 것이 바로 마침표입니다. 한마디로 화살표(→)라고 생각하면 이해가 빠를 겁니다. 이와 같은 방식은 C#도 그렇고 swift도 마찬가지죠. 좀 더 어려운 예제를 보죠.
우리는 아래와 같이 주어진 문제를 코딩해야한다고 가정해봅시다.
"정수 3개 A, B, C를 입력받은 뒤 A, B, C와 세 변수의 합 D를 하나의 문자열로 묶은 뒤 한 번에 출력하여라. (출력 방식 : A+B+C=D)"
여기서 포인트는 하나의 문자열로 묶어야 한다는 점일 것입니다. 자바에서는 문자열을 수정, 변형을 용이하게 하기 위한 StringBuilder라는 라이브러리가 있습니다. 이 것을 이용해봅시다.
// 자바 언어
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int A, B, C;
A = in.nextInt();
B = in.nextInt();
C = in.nextInt();
StringBuilder sb = new StringBuilder();
sb.append(A);
sb.append("+");
sb.append(B);
sb.append("+");
sb.append(C);
sb.append("=");
sb.append(A + B + C);
System.out.print(sb.toString());
}
}
이렇게 짜면 순차적으로 짜여져있기 때문에 이해하기 쉬울겁니다. 대신 줄이 좀 길어지게 되죠. 여기서 문자열을 묶는 과정에서 append()라는 메소드가 여러 번 반복되니 한 번에 묶을 순 없을까요?
Java API를 보시면 다음과 append()함수는 다음과 같은 타입을 반환(return)한다고 쓰여있습니다.
docs.oracle.com/javase/7/docs/api/java/lang/StringBuilder.html#append(int)
보면 public StringBuilder append(int i) 형태라고 하죠. 즉, 반환형태가 StringBuilder 라는 객체타입이라는 뜻입니다. 아까 마침표(.)는 객체의 메소드 또는 변수(필드)를 호출 할 수 있다고 했으니, 반환되는 타입에 대해서도 메소드 또는 변수를 호출 할 수 있습니다.
즉, 위 객체에서는 StringBuilder.append() 를 호출하면 반환되는 타입이 StringBuilder이기 때문에 이에 대해 또 다시 append()를 사용 할 수 있다는 뜻입니다. 쉽게 보면 StringBuilder.append().append(); 이런식으로 묶을수도 있죠.
즉, 아래와 같이 쉽게 묶을 수 있습니다.
// 자바 언어
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int A, B, C;
A = in.nextInt();
B = in.nextInt();
C = in.nextInt();
StringBuilder sb = new StringBuilder();
System.out.print(sb.append(A).append("+").append(B).append("+").append(C).append("=").append(A + B + C).toString());
}
}
보시다시피 '함수의 기능을 연속적이게 한 줄로 묶어'줍니다.
StringBuilder라는 문자열을 쉽게 조작할 수 있는 객체를 sb라는 이름으로 정해주고, sb라는 객체에 append()로 이어 붙여준 뒤 "+"라는 문자를 붙여주고.. 이와 같은 과정을 반복하여 C까지 입력받은 뒤 세 수의 합을 구하고 문자열로 반환해주는 방식이죠.
즉, 반환타입에 대한 메소드 또는 필드도 호출할 수 있습니다.
위 예제를 swift로 한다면 다음과 같을 겁니다.
import Foundation
var val = readLine()?.split(separator: " ").map { Int($0) }
var a = String(val![0]!) + " + " + String(val![1]!) + " + " + String(val![2]!) + " = " + String(val![0]! + val![1]! + val![2]!)
print(a)
여기서도 마찬가지로 readLine()으로 한 줄을 읽는다(String 타입 반환) → 읽은 문자열을 공백(" ")을 기준으로 분리시킨다(SubString(배열)반환) → Int(정수)로 매핑한다(int 배열반환). 왼쪽에서 오른쪽으로 순차적으로 진행되죠. 우리가 글을 읽을 때처럼 왼쪽에서 오른쪽으로 진행합니다.
이 부분이 중요한 이유는 지금 당장은 여러분들이 현재 배우고 있는 언어에서 제공하는 라이브러리를 사용하시겠지만, 나중에 사용자 정의 함수를 만들게 되면 위와 같이 길게 쓸 일이 생각보다 많이 존재합니다.
(참고로 물음표 키워드는 Swift의 nil이라는 것인데 쉽게 생각하면 값(예제에서는 입력)이 있을수도, 없을수도 있는 상태라고 보시면 됩니다. 느낌표의 경우는 강제 언래핑(Unwrapping)이라고 보시면 됩니다.)
앞서 우리가 수식 파트에서 얘기한 것을 함수로 표현 한 것은 h(g(f(x))) = f →g→h 이였습니다. 가장 안쪽 함수부터 찾아 나서야 했죠. 반대로 위와같이 .(마침표)로 연결시키면 좀 더 직관적으로 표현할 수 있습니다. 예로 들어 f(x).h(x).g(x) 라고 한다면 동작 순서는 써진 절차 그대로 f →h→g 가 되죠.
정리하자면 이렇습니다. 한 문장의 기본 구조는 왼쪽에서 오른쪽으로 순차적으로 진행되고, 단어들을 한 문장으로 연결해주는 문자가 여러 기능들을 직관적으로 이해하기 쉽게 묶어주는 역할을 담당한다.
오늘은 문법중 '절차'에 대해 알아보았습니다.
키워드의 나열의 경우 말 그대로 어떤 특정 상태를 정의하는데에 필요한 키워드들을 나열할 수 있다는 것이고 객체 및 메소드 연결의 경우 어떤 것에 대해 접근(호출)하기 위해 사용되는 것은 마침표(.)이고, 마침표를 통해 반환되는 값의 객체 타입에 대해서도 변수 또는 메소드들을 호출 할 수 있다는 것입니다.
저번 포스팅을 포함하여 보자면 크게 세 가지를 설명했죠. 단어와 문법('수식'과 '절차')
단어는 어떠한 기능을 해주기 위한 최소한의 단위구조(문법)을 의미한다.
수식은 h(g(f(x))) = f →g→h 로 표현할 수 있다.
절차는 f(x).g(x).h(x) = f →g→h 로 표현할 수 있다.
여기서 하나 첨언을 하자면 수식의 h(g(f(x))) 같은 형식은 '매개변수 타입'에 종속되고, 절차의 f(x).g(x).h(x)는 '반환 타입'에 종속된다고 보시면 될 것 같습니다.
위 세 가지만 정확히 이해하신다면 길거나 복잡하게 짜여 있는 소스들을 적어도 구조를 개괄적으로 분석할 수 있을 겁니다.
어느 정도 감이 오셨으리라 봅니다. 이렇게 각각의 단어들이 모여 하나의 연결된 문장으로, 우리가 쓰는 언어와 크게 구조가 다르지 않습니다. 여러분들이 설령 다른 언어를 접하시더라도 수식과 절차에 대한 기본 이해만 있다면 자신이 필요한 함수들만 잘 검색해서 찾아보기만 하면 최소한 기본 코딩은 가능할 겁니다. 그렇기 때문에 배운 그대로 복사 붙여 넣기 하듯이 학습을 하기보다는 이 문법이 왜 그런지, 무슨 기능을 하는지를 익히는 것이 중요하죠.
만약 이해가 되지 않거나 해석하기 어려운 코드 등 기타 질문이 있다면 얼마든지 댓글 남겨주시면 답변드리겠습니다. 그럼 이쯤에서 글을 마치도록 하겠습니다.
'프로그래밍 기초' 카테고리의 다른 글
메모리 구조 [Memory Structure] (84) | 2021.01.31 |
---|---|
2진수의 수와 음수 표현법 [1의 보수와 2의 보수] (73) | 2021.01.01 |
프로그래밍 언어와 빌드 과정 [Build Process] (22) | 2020.11.27 |
객체지향(OOP)과 절차적 프로그래밍(PP) (32) | 2020.10.24 |
프로그래밍 언어의 단어와 문법 - 1 [프로그래밍] (17) | 2020.10.24 |