이 글은 〈[되기] 코딩 테스트 합격자 되기(자바 편)〉에서 발췌했습니다.
골든래빗 출판사
김희성 지음
코딩 테스트 문제를 풀기 전에는 당연히 코딩 테스트에 사용할 언어의 문법을 알아야 합니다. 여기서는 자바 기초 문법을 충실히 설명하기보다는 코딩 테스트에 자주 사용하는 문법을 설명하는 데 집중합니다. 자바 기초서 1권을 완독했다는 가정하에 설명했으므로 참고하기 바랍니다.
코딩 테스트 필수 문법
1. 프리미티브 타입과 레퍼런스 타입
자바에는 int, long, float, double과 같은 프리미티브 타입(Primitive Type)과 Integer, Long, Float, Double과 같은 레퍼런스 타입(Reference Type)이 있습니다. 결론부터 말하자면 이름에서도 알수 있듯이 레퍼런스 타입은 참조형 변수이므로 프리미티브 타입보다 연산 속도가 더 느립니다. 따라서 이 책에서는 특별한 경우가 아니라면 대부분 프리미티브 타입으로 설명합니다. 하지만 레퍼런스 타입은 컬렉션 프레임워크 등에서 정수형 또는 부동소수형을 저장할 때 사용하기 때문에 반드시 알고 넘어가야 합니다.
1.1 정수형
정수형은 양과 음의 정수, 0을 포함합니다. 정수형은 더하기, 빼기, 곱하기, 나누기와 같은 사칙 연산 외 많은 연산을 할 수 있습니다. 이 책을 펼친 독자라면 대부분 설명 없이 코드만 봐도 쉽게 이해할 수 있을 것입니다. 코드양이 많아서 좀 지루할 수도 있습니다만 연산자는 글보다는 코드를 보며 공부하는 것이 훨씬 직관적이고 이해하기 쉬우니 꼭 살펴보기 바랍니다.
1.1.1 정수형 변수 선언
int a = 13
int b = 4
1.1.2 정수형 산술 연산
System.out.println(a + b); // 더하기 / 17
System.out.println(a - b); // 빼기 / 9
System.out.println(a * b); // 곱하기 / 52
System.out.println(a / b); // 나누기 (소수점 버림) / 3
System.out.println(a % b); // 모듈러 연산 (소수점 버림) / 1
1.1.3 정수형 비교 연산
System.out.println(a == b) # 같은 값인지 비교 / false
System.out.println(a != b) # 같지 않은 값인지 비교 / true
System.out.println(a > b) # 왼쪽 값이 더 큰지 비교 / true
System.out.println(a < b) # 왼쪽 값이 더 작은지 비교 / false
System.out.println(a >= b) # 왼쪽 값이 더 크거나 같은지 비교 / true
System.out.println(a <= b) # 왼쪽 값이 더 작거나 같은지 비교 / false
1.1.4 정수형 비트 연산
System.out.println(a & b) # AND / 4
System.out.println(a | b) # OR / 13
System.out.println(a ^ b) # XOR / 9
System.out.println(~a) # NOT / -14
System.out.println(a << 2) # 왼쪽 시프트 (a에 2^2를 곱한 것과 동일) / 52
System.out.println(a >> 1) # 오른쪽 시프트 (a를 2^1로 나눈 것과 동일) / 6
1.2 부동소수형
부동소수형은 소수를 저장할 때 사용합니다. 부동소수형도 연산 결과를 보며 설명하겠습니다.
1.2.1 부동소수형 사칙 연산과 모듈러 연산
System.out.println(2.5 + 3.7) # 더하기 / 6.2
System.out.println(7.9 - 4.2) # 빼기 / 3.7
System.out.println(1.5 * 4.8) # 곱하기 / 7.199999999999999
System.out.println(10.0 / 3.2) # 나누기 / 3.125
System.out.println(10.0 % 3.2) # 모듈러 / 0.39999999999999947
1.2.2 부동소수형 논리 연산
double x = 0.5;
double y = 1.2;
double z = 2.0;
System.out.println(x > y && y < z); // AND 연산 / false
System.out.println(x < y || y < z); // OR 연산 / true
System.out.println(!(x > y)); // NOT 연산 / true
부동소수형 코드 실행 결과를 보면 눈에 띄는 내용이 있습니다. 10 % 3.2의 연산 결과를 보면 결괏값이 0.4가 아니라 0.39999999999999947입니다.
1.2.3 엡실론을 포함한 연산에 주의하라
이런 이유는 자바는 부동소수형 데이터를 이진법으로 표현하기 때문입니다. 표현 과정에서 오차가 발생하는 것이죠. 이를 엡실론(Epsilon)이라고 합니다. 구체적인 내용은 문법서에서 공부하는 것이 더 적합하므로 여기서는 생략하겠습니다. 제가 이 내용을 언급한 이유는 코딩 테스트에서 부동소수형 데이터를 다룰 일이 생겼을 때 이 엡실론을 항상 생각하라는 이유에서입니다. 여러분이 부동소수형을 사용하여 코드를 작성하면 엡실론이라는 요소 때문에 일부 테스트 케이스가 통과하지 못할 수도 있으니 유의하기 바랍니다.
* 마찬가지의 이유로 0.1을 3번 더한 a의 값에 0.3을 빼면 0이 아닙니다.
public class Solution {
public static void main(String[] args) {
double epsilon = 1E-5;
// 앱실론 출력
System.out.println(epsilon); // 1.0E-5
// 부동소수점 수 오차 검사
double a = 0.1 + 0.2;
double b = 0.3;
System.out.println(a - b); // 5.551115123125783E-17 ❶ 0이 아니네요!
if (a == b) {
System.out.println("a와 b는 같은 값입니다.");
} else {
System.out.println("a와 b는 다른 값입니다."); // 이 코드가 출력됨 ❷ 단순 a==b 비교하면 의도한 결과가 나오지 않습니다.
}
if (Math.abs(a - b) < epsilon) {
System.out.println("a와 b는 같은 값입니다."); // 이 코드가 출력됨 ❸ 이렇게 비교해야 의도한 결과가 나옵니다.
} else {
System.out.println("a와 b는 다른 값입니다.");
}
}
}
부동소수형 데이터를 활용하는 문제는 오차 허용 범위를 언급하는 경우가 많습니다. 문제를 분석할 때 꼭 이 부분을 체크하기 바랍니다. 이 지점에서 정말 많은 사람이 실수합니다.
다음은 컬렉션 프레임워크에서 래퍼런스 타입 사용에 대한 예입니다. 다음 장에서 다룰 컬렉션 프레임워크에서 사용해야 되기 때문에 꼭 기억하고 넘어가주세요. long을 선언할 때 프리미티브 타입이 필요한 상황에서는 꼭 소문자 l로 시작하는 long을 사용해주세요. 실수하기 쉬운 부분입니다.
// 프리미티브 타입 사용법
int i = 0;
long[] longs = new long[10];
float f = 10.5f;
double d = 10 / 3.0;
// 레퍼런스 타입 사용법(꼭 필요한 경우가 아니면 권장하지 않음)
Integer I = 0;
Long[] Longs = new Long[10];
Float F = 10.5f;
Double D = 10 / 3.0;
// 문법 오류 발생
ArrayList<int> arrayList = new ArrayList<>();
Stack<long> stack = new Stack<>();
Queue<float> queue = new Queue<>();
ArrayDeque<double> arrayDeque = new ArrayDeque<>();
// 올바른 코드
ArrayList<Integer> arrayList = new ArrayList<>();
Stack<Long> stack = new Stack<>();
Queue<Float> queue = new ArrayDeque<>();
ArrayDeque<Double> arrayDeque = new ArrayDeque<>();
2. 컬렉션 프레임워크
컬렉션 프레임워크는 여러 개의 값을 저장하고 그 값을 쉬우면서도 효율적으로 처리해주는 표준화 클래스의 집합입니다. 좀 더 쉽게 설명하면 컬렉션 프레임워크는 코딩 테스트 문제를 해결하기 위한 다양한 자료구조인 리스트, 큐, 스택, 해시맵 등을 직접 구현하지 않고도 손쉽게 사용할 수 있게 해줍니다. 대표적인 컬렉션 프레임워크에는 리스트(ArrayList), 스택(Stack), 큐(Queue), 데크(ArrayDeque), 해시맵(HashMap) 등이 있습니다.
2.1 배열
자바의 컬렉션 프레임워크 중 리스트에 대해 설명하기 전에 배열을 설명하고 넘어가겠습니다. 배열은 기초 자료형으로 저장할 데이터의 개수가 정해져 있을 때 사용하기에 유용합니다. 다음 코드를 보면 배열의 동작 방식을 쉽게 알 수 있을 것입니다.
* Arrays 클래스의 toString( ) 메서드를 사용하면 배열 요소를 쉽게 출력할 수 있어서 디버깅에 용이합니다.
import java.util.Arrays;
public class Solution {
public static void main(String[] args) {
int[] array = { 1, 2, 3, 4, 5 };
int[] array2 = new int[] { 1, 3, 5, 7, 9 };
int[] array3 = new int[5];
array3[0] = 0;
array3[1] = 2;
array3[2] = 4;
array3[3] = 6;
array3[4] = 8;
System.out.println(Arrays.toString(array)); // [1, 2, 3, 4, 5]
System.out.println(Arrays.toString(array2)); // [1, 3, 5, 7, 9]
System.out.println(Arrays.toString(array3)); // [0, 2, 4, 6, 8]
}
}
2.1.1 배열의 인덱스
배열의 인덱스는 특정 원소 위치에 빠르게 접근하기 위한 기능입니다. 예를 들어 다음 코드를 보면 3번째 인덱스에 접근하기 위해 myArray[2] 코드를 작성했는데, 이를 ‘인덱스로 배열의 원소에 접근한다’라고 표현합니다. 그리고 배열은 생성 이후에는 배열 크기를 변경할 수 없습니다. 따라서 배열 생성 후 새 데이터를 삽입하거나 삭제하는 것은 할 수 없고, 기존 데이터의 변경만 할 수 있습니다. 인덱스를 이용한 배열 요소에 대한 접근, 변경의 시간 복잡도는 O(1)입니다.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
// 배열 생성
int[] myArray = { 1, 4, 2, 5, 3 };
// 2번째 값을 7 변경
myArray[1] = 7;
System.out.println(Arrays.toString(myArray)); // [1, 7, 2, 5, 3]
// 1번째 값에 접근 및 출력
System.out.println(myArray[2]); // 2
// 5번째 값을 3번째 값으로 변경
myArray[4] = myArray[2];
System.out.println(Arrays.toString(myArray)); // [1, 7, 2, 5, 2]
}
}
2.2 리스트
자바의 리스트는 코딩 테스트 기준으로는 일반적으로 ArrayList를 의미합니다. 그래서 코딩 테스트에서는 리스트를 ArrayList로 구현하죠. 배열과 ArrayList의 가장 큰 차이점은 배열은 크기(Size)가 고정되어 있어서 데이터를 삭제하거나 삽입할 수 없지만, ArrayList는 가변 크기이므로 새 데이터의 삽입 혹은 기존 데이터의 삭제를 할 수 있습니다. 다만 ArrayList는 새 데이터를 맨 뒤에 추가할 때는 평균 시간 복잡도가 O(1)이며, 기존 데이터의 삭제 혹은 데이터를 중간에 삽입할 때는 시간 복잡도가 O(N)까지 커질 수 있으므로 주의가 필요합니다. 다음 코드를 보면 ArrayList의 동작 방식을 쉽게 알 수 있습니다.
// 리스트 객체 생성
ArrayList<Integer> list = new ArrayList<>();
// 값 추가
list.add(1);
list.add(2);
list.add(4);
list.add(6); // ❶
System.out.println(list.get(2)); // ❷ 4, 인덱스로 값에 접근
System.out.println(list); // ❸ [1, 2, 4, 6]
➊은 6을 리스트의 맨 뒤에 추가합니다.
➋는 인덱스 2의 데이터 4를 읽습니다.
➌과 같이 리스트 전체를 출력하면 [1, 2, 4, 6]입니다.
리스트는 할 이야기가 조금 더 있습니다만 코딩 테스트에 필요한 설명은 이 정도면 충분합니다.
2.3 해시맵
자바의 해시맵(Hashmap)은 키(Key)와 값(Value) 쌍을 저장하는 해시 테이블로 구현되어 있습니다. 키를 사용하여 값을 검색하는 자료구조라고 생각하면 됩니다. 예시 코드를 그림과 함께 살펴보겠습니다.
2.3.1 해시맵 초기화
다음은 키는 문자열을, 값은 32비트 정수형을 저장하는 해시맵을 하나 선언한 모습입니다.
HashMap<String, Integer> map = new HashMap<>();
2.3.2 해시맵의 데이터 삽입과 출력
다음 코드는 해시맵에 값을 삽입하고 해시맵에 들어 있는 데이터 전체를 출력합니다.
// 해시맵 값 삽입
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
// 해시맵 값 출력
System.out.println(map); // {banana=2, orange=3, apple=1}
2.3.3 해시맵의 데이터 검색
해시맵에 “apple” 문자열과 일치하는 키가 있는지 확인하고, 일치하는 키를 찾으면 키-값을 출력합니다.
String key = "apple";
if (map.containsKey(key)) {
int value = map.get(key);
System.out.println(key + ": " + value); // apple: 1
}
else {
System.out.println(key + "는 해시맵에 없습니다.");
}
2.3.4 해시맵 수정
키 “banana”를 검색하여 해당 키의 값을 4로 바꿉니다. 해시맵 내부는 해시 테이블로 구성되어 있으므로 해시 테이블에서 키를 찾거나 하지 않아도 됩니다.
map.put("banana", 4);
System.out.println(map); // {banana=4, orange=3, apple=1}
2.3.5 해시맵 삭제
map.remove("orange");
System.out.println(map); // {banana=4, apple=1}
// 해시맵 객체 생성
HashMap<String, Integer> map = new HashMap<>();
// 해시맵 값 삽입
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
// map에 없는 키로 설정
String key = "orange";
// 키가 해시맵에 있는지 확인
if (map.containsKey(key)) {
// 키가 해시맵에 있으면 해당 값 출력
System.out.println("값: " + map.get(key));
}
else {
// ➊ 키가 해시맵에 없으면 오류 메시지 출력
System.out.println(key + "는 해시맵에 없습니다.");
}
➊ 해시맵에 키가 없는 경우를 처리하는 예외 처리입니다. 키가 없는 경우 예외가 발생하므로 예외처리를 해주어야 합니다.
참고로 자바에는 해시맵과 유사하지만 ‘키-값’ 쌍으로 저장하지 않고 ‘값’ 없이 ‘키’만 저장하는 해시셋도 있습니다.
2.4 문자열
문자열은 문자들을 배열의 형태로 구성한 이뮤터블 객체입니다.
2.4.1 문자열 초기화
자바에서 문자열은 이뮤터블 객체입니다. 이뮤터블 객체는 값을 변경할 수 없는 객체를 의미하고 시간 복잡도 관점에서 사용 시 주의해야 할 필요가 있습니다. 면접에서도 자주 물어보는 주제이므로 자바에서 문자열을 다루는 방법은 자세하게 알아두면 좋습니다. 문자열은 큰따옴표로 감싸 사용합니다.
String string = "Hello, World!";
2.4.2 문자열 추가, 삭제
다음은 문자열을 추가하고 삭제하는 동작입니다. 여기서 주목해야 할 점은 문자열은 이뮤터블 객체이므로 기존 객체를 수정하는 것이 아니라 새로운 객체를 반환한다는 사실입니다.
String string = "He"; // ➊
string += "llo"; // ➋
System.out.println(string); // "Hello"
➊ 문자열을 초기화합니다. string이 문자열 “He”를 참조합니다.
➋ 이어서 string이 참조하는 “He”와 “llo”를 합쳐 새로운 문자열을 만들고 string은 그 문자열을 참조합니다.
2.4.3 문자열 수정
그렇다면 문자열을 수정하고 싶다면 어떻게 해야 할까요? 그럴 때는 replace( ) 메서드를 사용하면 됩니다. replace( ) 메서드는 첫 번째 인수에 찾을 문자열을, 두 번째 인수에 변경할 문자열을 넣어 사용합니다.
예를 들어 replace(A, B)는 replace( ) 메서드의 대상 문자열에서 A를 모두 찾아 B로 변경합니다.
String string = "Hello";
string = string.replace("l", ""); // "l"을 모두 삭제
System.out.println(string); // "Heo"
2.4.4 StringBuffer와 StringBuilder
앞서 언급했던 것처럼 자바에서 String 객체는 값을 변경할 수 없는 Immutable 객체입니다. ‘값을 변경할 수 없다’는 것이 어떤 의미인지 자세히 알아보기 위해 다음 코드를 보겠습니다.
String s = "abc";
System.out.println(System.identityHashCode(s)); // 1808253012
s += "def"; // ➊
System.out.println(System.identityHashCode(s)); // 589431969
System.out.println(s); // abcdef
System.identityHashCode( ) 메서드는 객체를 특정할 수 있는 식별값을 반환합니다. String s의 abc에 def를 이어 붙였을뿐인데 identityHashCode( ) 메서드의 출력값이 달라졌습니다. 이는 “abc”값만 가지고 있던 s와 “abcdef”값을 가지고 있는 s가 서로 다른 객체임을 의미합니다. 즉, String 객체의 값을 변경하는 작업은 새로운 String 객체를 만들고 값을 복사하는 작업이 수행됨을 의미합니다. 따라서 ➊ s += “def”를 수행할 때 내부에서는 다음 연산이 수행됩니다.
- 새로운 String s 객체를 생성
- s가 가진 “abc”값을 하나씩 복사
- “abc” 뒤에 “def” 저장
그 결과 ➊ s += “def” 코드 한 줄에서 총 6번의 내부 연산(“abc”값 3개 복사, “def”값 3개 저장)이 수행됩니다. 시간 복잡도로 따지면 문자열의 길이를 N이라 했을 때 O(N)이 됩니다.
다음 코드를 수행하면 제 컴퓨터에서는 6.5초의 시간이 걸립니다. 코드의 시간 복잡도는 O(N2)이므로 상당히 오랜 시간이 걸린 것입니다.
- 다음 코드에서 N은 100,000입니다.
long start = System.currentTimeMillis();
String s = "";
for (int i = 1; i <= 100000; i++) {
s += i;
}
long end = System.currentTimeMillis();
System.out.println(((end - start) / 1000.0) + "초");
이러한 문제를 해결하기 위해 나온 것이 StringBuilder 클래스와 StringBuffer 클래스입니다. StringBuilder 클래스와 StringBuffer 클래스는 뮤터블하므로 값을 변경할 때 시간 복잡도 관점에서 훨씬 더 효율적입니다. 다음 코드는 아까 작성한 코드와 비교했을 때 s 객체에 저장한 문자열은 동일합니다. 하지만 실행 시간은 완전히 다르죠. 0.005초의 시간이 소요됩니다.
long start = System.currentTimeMillis();
StringBuilder s = new StringBuilder();
for (int i = 1; i <= 100000; i++) {
s.append(i);
}
long end = System.currentTimeMillis();
System.out.println(((end - start) / 1000.0) + "초");
따라서 String의 값을 변경하는 연산이 많을 때는 효율이 높은 StringBuilder 클래스나 StringBuffer 클래스를 사용해야 합니다. 두 클래스의 차이는 멀티스레드 환경에서 Thread-Safe 여부로 나뉩니다. 하지만 대부분의 코딩 테스트에서는 다수의 스레드를 생성할 필요가 없습니다. 결론은 Thread-Safe가 없는 StringBuilder 클래스가 속도 측면에서 미세하지만 더 빠르므로 StringBuilder를 사용하면 됩니다.
2.4.5 StringBuilder 클래스의 활용 방법
다음은 StringBuilder 클래스의 사용 방법입니다. 아마 복잡하지 않아 쉽게 이해할 수 있을 겁니다. String값을 변경하는 연산이 많을 때는 시간 초과가 발생하지 않도록 꼭 String 대신 StringBuilder를 사용해주세요.
// StringBuilder 객체 생성
StringBuilder sb = new StringBuilder();
// 문자열 Add
sb.append(10);
sb.append("ABC");
// 출력
System.out.println(sb); // 10ABC
sb.deleteCharAt(3); // 3번째 인덱스 문자 삭제
System.out.println(sb); // 10AC
sb.insert(1, 2); // 1번째 인덱스에 2라는 문자 추가
System.out.println(sb); // 120AC
3. 매서드
메서드는 프로그램의 중요한 요소입니다. 여기서는 메서드와 관련하여 주요 내용과 코딩 테스트를 위해 알아야 할 내용만 빠르게 공부하고 넘어가겠습니다.
3.1 매서드 정의
자바의 메서드는 다음과 같이 정의합니다. 자바 문법을 안다면 다음 코드는 아주 익숙할 것이므로 간단히 살펴보고 넘어가겠습니다.
* 엄밀히 말하자면 메서드는 클래스 내부에 정의한 함수를 말합니다. 자바에서는 대부분 클래스 내부에 함수를 정의할 것이므로 메서드라고 부르겠습니다.
public int function_name(int param1, int param2) {
// 메서드의 실행 코드
// ...
// ...
return result; // 반환값
}
3.2 메서드 호출
메서드를 정의했으면 메서드를 호출할 수 있습니다. 메서드를 호출할 때 매개변수가 있는 경우 add(5, 10)과 같이 인수를 함께 전달합니다.
public static void main(String[] args) {
// 함수를 호출하여 결과 출력
int ret = add(5, 10);
System.out.println(ret);
}
public static int add(int num1, int num2) {
int result = num1 + num2;
return result;
}
3.3 람다식
람다식(Lambda Expression)은 자바 1.8 버전에서 추가되었습니다. 람다식은 다른 말로 익명 함수 (Anonymous Function)라고도 합니다. 익명 함수란 말 그대로 이름이 없는 함수를 말하며 코드에서 딱 한 번 실행할 목적으로 사용하거나 함수 자체를 다른 함수의 인수로 전달할 때도 사용할 수 있습니다. 또한 람다식을 사용하면 함수를 더 간결하게 표현할 수 있고 가독성이 좋아진다는 장점이 있습니다.
3.3.1 람다식 정의와 사용
람다식은 다음과 같이 정의하여 사용할 수 있습니다. 다음 코드는 int형 dest와 cost를 멤버 변수로 갖는 Node 클래스의 객체를 생성하여 배열에 담고 그 배열의 객체들을 cost 기준으로 오름차순 정렬합니다.
private static class Node {
int dest, cost;
public Node(int dest, int cost) {
this.dest = dest;
this.cost = cost;
}
}
public static void main(String[] args) {
Node[] nodes = new Node[5];
nodes[0] = new Node(1, 10);
nodes[1] = new Node(2, 20);
nodes[2] = new Node(3, 15);
nodes[3] = new Node(4, 5);
nodes[4] = new Node(1, 25);
Arrays.sort(nodes, (o1, o2) -> Integer.compare(o1.cost, o2.cost)); // ➊
Arrays.sort(nodes, new Comparator<Node>() {
@Override
public int compare(Node o1, Node o2) {
return Integer.compare(o1.cost, o2.cost);
}
}); // ➋
}
위 코드에서 ➊과 ❷는 정확히 동일한 로직을 수행합니다. 하지만 코드의 길이는 ➊이 더 짧으면서 가독성도 좋습니다. 코딩 테스트에서 람다식을 사용하는 일은 극히 드물지만 이렇게 정렬 API를 사용할 때 간혹 사용하면 좋습니다.
Integer.compare(int x, int y) 메서드는 다음과 같이 동작합니다.
- x < y면 -1 반환
- x > y면 1 반환
- x == y면 0 반환
간혹 정렬 조건을 구현할 때 [x – y]와 같이 뺄셈을 하는 경우가 있는데, 이렇게 하면 오버플로의 위험이 있습니다. 그래서 자료형에 맞는 compare( ) 메서드를 사용하는 것이 좋습니다.
4. 코딩 테스트 코드 구현 노하우
코딩 테스트를 처음 공부하면 만나는 첫 난관은 코드 구현입니다. 자료구조나 알고리즘은 이론 지식이므로 공부하면 지식이 쌓이면서 실력이 늡니다. 하지만 코드 작성 노하우는 쉽게 늘지 않습니다. 여기서는 코딩 테스트에 유용한 코드 작성 노하우를 몇 가지 소개하겠습니다. 이런 노하우는 하루만에 습득하기 어렵습니다. 습관이 되어야 하므로 코드를 작성할 때마다 적용해보기 바랍니다.
4.1 조기 반환
조기 반환(Early Return)은 코드 실행 과정이 함수 끝까지 도달하기 전에 반환하는 기법입니다. 이 방식은 코드의 가독성을 높여줄 뿐만 아니라 예외를 조금 더 깔끔하고 빠르게 처리할 수 있습니다.
public static void main(String[] args) {
System.out.println(totalPrice(4, 50));
}
static int totalPrice(int quantity, int price) {
int total = quantity * price; // ➊
if (total > 100) // ➋
return (int)(total * 0.9); // ❸
return total;
}
➊ total에 quantity * price를 대입합니다.
➋ total의 값이 100보다 큰 경우 ➌ total에 0.9를 곱하고 반환합니다. 이렇게 하면 함수 자체를 조기에 종료할 수 있으므로 이후 예외에 대한 처리를 하지 않아도 됩니다.
4.2 보호 구문
보호 구문(Guard Clauses)은 본격적인 로직을 진행하기 전 예외 처리 코드를 추가하는 기법입니다. 예를 들어 조건문을 이용하여 초기에 입력값이 유효한지 검사하고 그렇지 않으면 바로 함수를 종료하는 보호 구문을 쓸 수 있습니다.
import java.util.List;
static double calculateAverage(List<Integer> numbers) {
if (numbers == null) // ➊ null 이면 종료(예외)
return 0;
if (numbers.isEmpty()) // ➋ 데이터가 없으면 종료(예외)
return 0;
int total = numbers.stream().mapToInt(i -> i).sum(); // ➌ 예외 처리 후 기능 구현
return (double) total / numbers.size();
}
이렇게 구현한 코드는 보호 구문 이후 구현부에서 입력값에 대한 예외를 고려하지 않아도 되므로 보기 좋습니다. 추가로 이런 습관을 들이면 처음부터 예외를 고려할 수 있어 코드를 더 안전하게 작성할 수 있게 됩니다. 코드를 보면 ➊, ➋에서 예외 처리를 하여 함수를 종료시킵니다. 여기서 예외를 잘 고려했다면 이후 코드에서는 ➌과 같이 원하는 동작 구현에만 집중하면 되겠죠.
4.3 제네릭
제네릭(Generic)은 빌드 레벨에서 타입을 체크하여 타입 안정성을 제공하고, 타입 체크와 형변환을 생략할 수 있게 해주어 코드를 간결하게 만들어주는 기능입니다.
List list = new ArrayList();
list.add(10);
list.add("abc");
int sum1 = (int)list.get(0) + (int)list.get(1); // ➊ 런타임 오류 발생
List<Integer> genericList = new ArrayList<>();
genericList.add(10);
genericList.add("abc"); // ➋ 문법(빌드 레벨) 오류 발생
int sum2 = genericList.get(0) + genericList.get(1);
코드를 보면 ➊에서는 런타임 오류가 발생하고, ➋에서는 빌드 오류가 발생합니다. List를 정의할 때 <Integer>와 같이 타입을 강제하는 것을 제네릭이라고 합니다. 제네릭은 타입에 맞지 않는 데이터를 추가하려고 할 때 문법 오류를 발생시켜 개발자의 실수를 방지해줍니다.
따라서 ➊은 코드를 실행해야만 오류가 발생한다는 것을 알 수 있지만, ➋는 빌드 자체가 안되므로 런타임 버그를 방지할 수 있습니다. 또한 데이터에 접근하여 사용하려고 할 때 형변환을 할 필요가 없기 때문에 코드가 간결해집니다. 코딩 테스트에서는 여러 타입의 데이터를 하나의 컬렉션에 넣어야 하는 경우는 거의 없으므로 제네릭으로 타입을 강제하여 실수를 방지하는 것이 좋습니다.
5. 리마인드
기억 01
자바의 데이터 타입에는 프리미티브 타입과 레퍼런스 타입이 있습니다. 프리미티브 타입의 연산 속도가 레퍼런스 타입에 비해 더 빠르기 때문에 가능하다면 프리미티브 타입을 사용하는 것이 좋습니다.
기억 02
자바의 컬렉션 프레임워크의 리스트, 해시맵은 코딩 테스트에서 자주 사용하므로 꼭 알아두면 좋습니다. 그 밖에 다른 컬렉션들은 다음 장에서 배웁니다.
기억 03
자바에서 String 클래스의 객체는 Immutable 객체입니다. 값을 변경할 수 없기 때문에 + 연산 등으로 값을 변경할 경우 효율이 좋지 않습니다. String의 + 연산이 자주 발생한다면 StringBuilder를 사용해주세요.
기억 04
메서드는 길어질 수 있는 코드를 논리적으로 분리할 수 있게 해줍니다. 잘 활용하시면 코드의 중복을 줄이고 가독성을 높일 수 있습니다.
기억 05
조기 반환, 보호 구문, 제네릭 등의 기법을 활용하면 코드의 가독성과 효율성을 높일 수 있습니다.
지금까지 코딩 테스트에 자주 사용하는 자바 필수 문법을 알아보았습니다. 코딩 테스트는 기본 지식만으로는 통과하기 어렵습니다. 시험을 보기 위해 공부를 따로 하는 것처럼 코딩 테스트도 별도로 공부가 필요합니다. 《코딩 테스트 합격자 되기》(자바 편)과 함께 코딩 테스트를 보기 전에 알아두면 좋을 노하우와 여러 지식을 익히고, 자료구조, 알고리즘 이론 개념 및 실전 시험 같은 모의고사로 꼭 대비하시길 권합니다.
자료구조, 알고리즘, 빈출 97 문제로 대비하는 코테 풀 패키지
[되기] 코딩 테스트 합격자 되기(자바 편)
저자 김희성
현 42dot 백엔드 개발자. 이전에는 삼성SDS에서 소프트웨어 개발자, 쿠팡에서 풀스택 개발자로 근무했다. 특히 삼성SDS 시절에는 사내 SW역량테스트 강사로 활약했다. 귀찮은 거 싫어하고 집에서 자는 게 가장 좋은 백엔드 개발자다. 어려운 문제와 맞닥뜨렸을 때 더욱 불타오르는 타입. 새벽 시간에 코드짜는 걸 좋아하며 주말에 밤새 코딩하는 일을 즐기는 ESTJ.