출제기준 세세항목: 절차적 프로그래밍 언어 · 객체지향 프로그래밍 언어 · 스크립트 언어 · 선언형 언어
프로그래밍 언어는 문제를 푸는 방식에 따라 크게 둘로 나눕니다. 명령형 언어는 컴퓨터에게 '이것을 하고, 다음에 저것을 하라'고 처리 순서를 하나하나 지시합니다. 절차적 언어와 객체지향 언어가 여기에 속합니다. 선언형 언어는 처리 순서 대신 '이런 결과를 원한다'는 목표를 적고, 어떻게 구할지는 언어나 시스템에 맡깁니다.
실행 방식으로도 나눕니다. 컴파일 언어는 실행 전에 소스 코드 전체를 기계어로 번역해 두고 그 결과를 실행합니다. 번역 시간이 들지만 실행이 빠르며, C가 대표적입니다. 인터프리터 언어는 미리 번역하지 않고 실행하면서 한 줄씩 해석하므로 고치고 바로 실행해 보기 편하지만 상대적으로 느립니다. Python과 JavaScript가 여기에 가깝습니다.
Java는 그 중간입니다. javac 컴파일러가 소스 코드를 특정 기계에 묶이지 않은 바이트코드(.class 파일)로 번역하고, 각 운영체제에 설치된 자바 가상 머신(JVM)이 이 바이트코드를 실행합니다. 그래서 같은 바이트코드를 윈도우와 리눅스에서 그대로 실행할 수 있는데, 이를 플랫폼 독립성이라고 합니다. 또 쓰지 않는 메모리를 자동으로 치워 주는 가비지 컬렉션이 있어 개발자가 메모리를 직접 풀어 줄 필요가 없습니다.
절차적 프로그래밍 언어는 프로그램을 함수(프로시저)들의 모음으로 보고, 데이터를 함수에 넘겨 순서대로 처리합니다. 요리책의 레시피처럼 위에서 아래로 단계를 따라가는 방식입니다. C, 포트란(FORTRAN), 코볼(COBOL), 파스칼(Pascal), 베이직(BASIC) 등이 있습니다. 그중 C는 1970년대 초 벨 연구소에서 유닉스 운영체제를 만들기 위해 개발되었고, 하드웨어 가까이에서 효율적으로 동작해 운영체제와 임베디드 분야에서 지금도 많이 씁니다.
C의 가장 큰 특징은 포인터입니다. 포인터는 값 대신 값이 저장된 메모리 주소를 담는 변수입니다. &x는 x의 주소를, *p는 p가 가리키는 곳의 값을 뜻합니다. int x = 10; int *p = &x; *p = 20;을 실행하면 p를 통해 x가 바뀌어 x는 20이 됩니다. 배열 이름은 첫 칸의 주소처럼 쓰이므로 a[i]와 *(a + i)는 같은 값이고, int *p = a;라면 p[i]와 *(p + i)도 같은 값입니다. p + 1은 주소에 1바이트가 아니라 자료형 크기(int라면 4바이트)만큼 더한 다음 칸을 가리킵니다.
포인터는 함수에 값을 넘기는 방식과도 이어집니다. C의 함수는 기본적으로 값을 복사해서 받으므로(값에 의한 호출), 함수 안에서 매개변수를 바꿔도 원래 변수는 그대로입니다. 두 변수를 맞바꾸는 swap 함수가 원본을 바꾸게 하려면 swap(&x, &y)처럼 주소를 넘기고, 함수 안에서 *a와 *b로 값을 바꿔야 합니다. C에는 참조에 의한 호출이 따로 없어, 이렇게 주소 값을 복사해 넘기는 방법으로 참조에 의한 호출처럼 동작하게 만듭니다.
구조체(struct)는 서로 다른 자료형의 값을 하나로 묶은 사용자 정의 자료형입니다. struct Point { int x; int y; };로 정의하고, 구조체 변수의 멤버는 점(.)으로, 구조체를 가리키는 포인터의 멤버는 화살표(->)로 접근합니다. p가 구조체 포인터라면 p->x와 (*p).x는 같은 뜻입니다. 구조체 포인터를 함수에 넘기면 함수 안에서 바꾼 값이 원본에 남습니다.
객체지향 프로그래밍 언어는 프로그램을 서로 메시지를 주고받는 객체들의 모임으로 봅니다. 클래스는 객체를 만들기 위한 설계도이고, 그 설계도로 실제로 만든 것이 객체(인스턴스)입니다. 붕어빵 틀과 붕어빵의 관계와 같습니다. 클래스에는 객체의 상태를 나타내는 속성(필드)과 객체가 할 수 있는 행동인 메서드가 들어 있습니다. Java, C++, C#, Python 등이 객체지향을 지원합니다.
객체지향의 특징은 네 가지로 정리합니다. 캡슐화는 데이터와 그 데이터를 다루는 메서드를 하나로 묶고, 내부는 감춘 채 정해진 창구(메서드)로만 쓰게 하는 것입니다. 이렇게 내부를 감추는 것을 정보 은닉이라 합니다. 상속은 부모 클래스의 속성과 메서드를 자식 클래스가 물려받는 것이고, 다형성은 같은 이름의 메서드가 객체에 따라 다르게 동작하는 성질입니다. 추상화는 공통된 핵심만 뽑아 간단히 나타내는 것입니다.
생성자는 객체를 만들 때 자동으로 한 번 호출되어 초기값을 정해 주는 특별한 메서드입니다. Java에서 생성자는 이름이 클래스 이름과 같고 반환형을 적지 않습니다. 생성자를 하나도 만들지 않으면 컴파일러가 매개변수 없는 기본 생성자를 자동으로 넣어 주지만, 하나라도 직접 만들면 기본 생성자는 자동으로 생기지 않습니다. 매개변수만 다르게 여러 생성자를 둘 수도 있습니다.
상속 관계에서는 자식 객체를 만들 때 부모의 생성자가 먼저 실행됩니다. 자식 생성자의 첫 줄에 부모 생성자를 부르는 super(...)가 없으면 컴파일러가 super()를 몰래 넣어 주므로, 부모에 매개변수 없는 생성자가 없으면 직접 super(값)를 써야 합니다. 같은 클래스의 다른 생성자를 부를 때는 this(...)를 쓰고, super(...)와 this(...)는 원칙적으로 생성자의 첫 문장에 둡니다. Java는 클래스를 하나만 상속(extends)할 수 있고, 여러 개를 물려받는 효과는 인터페이스를 여러 개 구현(implements)해서 얻습니다.
오버로딩(Overloading)은 한 클래스 안에 이름이 같은 메서드를 매개변수의 개수, 자료형, 순서만 달리해 여러 개 두는 것입니다. add(int a, int b)와 add(double a, double b)를 함께 두면, 넘기는 값의 자료형에 맞는 쪽이 호출됩니다. 반환형만 다른 것은 오버로딩으로 인정되지 않습니다. 같은 일을 하는 메서드를 이름 하나로 쓰게 해 주는 편의 기능이라고 보면 됩니다.
오버라이딩(Overriding)은 부모 클래스에서 물려받은 메서드를 자식 클래스가 이름과 매개변수를 똑같이 맞춰(반환형도 같거나 그 하위 타입으로) 다시 정의하는 것입니다. 동물 클래스의 sound()를 강아지 클래스가 '멍멍'으로, 고양이 클래스가 '야옹'으로 다시 정의하는 식입니다. 접근 범위를 부모보다 좁힐 수 없고, final이 붙은 메서드나 private 메서드는 오버라이딩할 수 없습니다. 오버로딩은 '옆으로 늘리기', 오버라이딩은 '위에서 받은 것을 덮어쓰기'로 구별하면 됩니다.
다형성이 코드 추적 문제에서 드러나는 곳은 Parent p = new Child();처럼 부모 타입 변수에 자식 객체를 담는 경우입니다. 이때 p.method()로 부르면 변수 타입이 아니라 실제 객체(Child)의 오버라이딩된 메서드가 실행됩니다. 이것을 동적 바인딩이라고 합니다. 반면 필드(p.x)와 static 메서드는 변수의 타입(Parent)을 기준으로 정해집니다. 자식에만 있는 메서드는 p로 바로 부를 수 없고, (Child)p처럼 형 변환해야 합니다.
구현 없이 선언만 한 메서드를 추상 메서드라 하고 abstract를 붙입니다. 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 하며, 직접 객체를 만들 수 없고 자식이 추상 메서드를 모두 구현해야 객체를 만들 수 있습니다. 인터페이스는 메서드의 모양만 정해 둔 약속으로, 클래스가 implements로 구현합니다. final은 클래스에 붙이면 상속 금지, 메서드에 붙이면 오버라이딩 금지, 변수에 붙이면 값 변경 금지(상수)입니다.
Java의 접근 제어자는 멤버를 어디서 쓸 수 있는지 정합니다. public은 어디서나, protected는 같은 패키지와 다른 패키지의 자식 클래스에서, 아무것도 적지 않은 default(package-private)는 같은 패키지 안에서만, private은 같은 클래스 안에서만 쓸 수 있습니다. 넓은 순서로 public > protected > default > private입니다. 보통 필드는 private으로 감추고, 값을 읽고 바꾸는 public 메서드(getter·setter)를 둡니다. 캡슐화를 코드로 구현하는 방법입니다.
static이 붙은 멤버는 객체마다 따로 생기지 않고 클래스에 하나만 있어 모든 객체가 함께 씁니다. 그래서 static 변수를 클래스 변수라고도 부르며, 한 객체가 값을 바꾸면 다른 객체에서도 바뀐 값이 보입니다. 객체를 만든 개수를 세는 카운터에 자주 쓰입니다. static 메서드는 객체 없이 클래스이름.메서드()로 부를 수 있지만, 특정 객체를 뜻하는 this를 쓸 수 없고 일반(인스턴스) 필드에 바로 접근할 수 없습니다. main 메서드가 static인 것도 객체를 만들기 전에 실행되어야 하기 때문입니다.
Java에서 기본 자료형 변수는 값 자체를, 참조형 변수는 객체가 있는 위치(참조)를 담습니다. 메서드에 인자를 넘길 때는 언제나 그 내용이 복사되어 넘어가는데, 참조형은 위치가 복사되므로 메서드 안에서 객체의 필드를 바꾸면 원래 객체도 바뀝니다. 하지만 메서드 안에서 매개변수에 새 객체를 넣는 것은 복사본만 바꾸는 것이라 원래 변수에는 영향이 없습니다.
문자열 비교도 이와 이어집니다. ==는 두 변수가 같은 객체를 가리키는지(참조)를, equals()는 내용이 같은지를 비교합니다. 같은 문자열 리터럴 "abc"는 문자열 상수 풀의 같은 객체를 함께 쓰므로 ==가 true가 되지만, new String("abc")는 새 객체를 만들어 ==로 비교하면 false입니다. 내용 비교에는 항상 equals()를 써야 합니다.
스크립트 언어는 별도의 컴파일 과정 없이 인터프리터가 소스 코드를 바로 해석해 실행하는 언어입니다. 짧은 코드로 빠르게 결과를 볼 수 있어 자동화, 웹 개발, 데이터 처리에 많이 씁니다. 실행되는 위치에 따라 웹 브라우저에서 도는 클라이언트 측 스크립트(JavaScript)와 서버에서 도는 서버 측 스크립트(PHP, Python, Ruby 등)로 나누기도 하며, 운영체제 명령을 묶어 실행하는 셸 스크립트(Bash 등)도 여기에 속합니다. JavaScript는 Node.js를 쓰면 서버에서도 실행됩니다.
Python은 중괄호 대신 들여쓰기로 블록을 구분하고, 변수에 자료형을 적지 않는 동적 타이핑 언어입니다. 묶음 자료형이 특히 중요합니다. 리스트(list, [1, 2, 3])는 순서가 있고 내용을 바꿀 수 있습니다. 튜플(tuple, (1, 2, 3))은 순서가 있지만 바꿀 수 없으며, 원소가 하나면 (1,)처럼 쉼표를 붙여야 합니다. 딕셔너리(dict, {'a': 1})는 키와 값의 쌍이고, 세트(set, {1, 2})는 중복을 허용하지 않고 순서가 없으며 &(교집합), |(합집합), -(차집합) 연산을 지원합니다.
슬라이싱은 a[시작:끝:간격]으로 일부를 잘라 냅니다. 끝 인덱스는 포함하지 않고, 생략하면 처음이나 끝까지를 뜻하며, 음수 인덱스는 뒤에서부터 셉니다. a = [10, 20, 30, 40, 50]일 때 a[1:4]는 [20, 30, 40], a[::2]는 [10, 30, 50], a[::-1]은 뒤집은 [50, 40, 30, 20, 10], a[-2:]는 [40, 50]입니다. 인덱스 하나로 범위를 넘으면 오류가 나지만, 슬라이싱은 범위를 넘어도 오류 없이 있는 만큼만 돌려줍니다. 리스트 내포 [x * x for x in range(5) if x % 2 == 0]는 [0, 4, 16]을 만듭니다.
Python도 참조에 주의해야 합니다. b = a는 리스트를 복사하는 것이 아니라 같은 리스트에 이름을 하나 더 붙이는 것이라, b.append(4)를 하면 a도 바뀝니다. 따로 복사하려면 a[:]나 list(a), a.copy()를 씁니다. 함수의 기본값으로 빈 리스트를 주면 그 리스트는 함수를 정의할 때 한 번만 만들어져 호출 사이에 계속 공유됩니다. 클래스는 생성자 __init__과 자기 자신을 뜻하는 self를 명시적으로 쓰며, 클래스 안에 바로 선언한 변수는 모든 객체가 공유하는 클래스 변수입니다. Python은 같은 이름으로 메서드를 다시 정의하면 앞의 것이 덮어써지므로 Java식 오버로딩이 없습니다.
선언형 언어는 처리 절차 대신 원하는 결과를 적습니다. 택시를 타고 '시청으로 가 주세요'라고만 말하고 길은 기사에게 맡기는 것과 같습니다. 반대로 명령형은 '직진하고, 두 번째 사거리에서 좌회전하고…'라고 길을 하나하나 알려 주는 방식입니다.
가장 익숙한 예는 SQL입니다. SELECT name FROM student WHERE grade = 3이라고 쓰면 3학년 학생의 이름을 원한다는 것만 말할 뿐, 어떤 순서로 테이블을 읽을지는 DBMS가 정합니다. 웹 페이지의 구조를 적는 HTML도 '여기에 제목, 여기에 목록'처럼 무엇이 있는지만 선언하므로 선언형으로 분류합니다.
함수형 언어는 계산을 수학의 함수처럼 다룹니다. 같은 입력에는 늘 같은 결과를 내고 바깥 상태를 바꾸지 않는 순수 함수, 한 번 만든 값을 바꾸지 않는 불변성, 함수를 값처럼 넘기고 돌려받는 고차 함수, 반복 대신 재귀를 즐겨 씁니다. 하스켈(Haskell), LISP, 얼랭(Erlang) 등이 있고, Python의 map·filter·lambda처럼 다른 언어에도 함수형 요소가 들어와 있습니다.
논리형 언어는 사실(fact)과 규칙(rule)을 선언해 두고, 질문을 던지면 시스템이 논리적으로 추론해 답을 찾습니다. '철수는 영희의 부모다', 'X가 Y의 부모이고 Y가 Z의 부모면 X는 Z의 조부모다'를 적어 두고 '철수는 누구의 조부모인가'를 묻는 식입니다. 대표 언어가 프롤로그(Prolog)이며, 인공지능과 전문가 시스템 분야에서 쓰였습니다.
카드, 계좌이체, 간편결제를 모두 결제(Payment) 클래스를 상속한 자식으로 만들고 pay() 메서드를 각각 오버라이딩했습니다. 주문 처리 코드는 Payment 타입 변수로 pay()만 부르면 실제 결제 수단에 맞는 메서드가 실행되어, 새 결제 수단이 생겨도 주문 코드는 고칠 필요가 없었습니다.
Java로 만든 관리자 로그인이 올바른 비밀번호를 넣어도 가끔 실패했습니다. 입력받은 문자열과 저장된 문자열을 ==로 비교해 내용이 아니라 객체 위치를 비교하고 있었기 때문이며, equals()로 바꾸자 해결되었습니다.
Python으로 원본 매출 목록을 b = a로 '복사'한 뒤 b에서 반품 건을 지웠더니 원본 a에서도 사라져 다른 보고서 숫자가 틀어졌습니다. b = a.copy()로 바꿔 두 목록을 따로 관리하게 했습니다.