Java 주니어 코딩 면접
새 면접Q. 주어진 문자열에서 중복된 문자를 제거하고 처음 등장한 순서대로 반환하는 메서드를 작성해야 합니다. 예를 들어 'programming'이 입력되면 'progamin'을 반환해야 합니다. 어떤 자료구조를 사용하고 어떤 방식으로 구현하시겠습니까?
LinkedHashSet을 사용하여 구현하는 것이 가장 효율적입니다. LinkedHashSet은 중복을 자동으로 제거하면서 삽입 순서를 유지하는 특성이 있습니다. 문자열을 문자 배열로 변환한 후 각 문자를 LinkedHashSet에 추가하고, 최종적으로 Set의 요소들을 StringBuilder로 결합하여 문자열로 반환합니다. 시간복잡도는 O(n)이며, 공간복잡도도 O(n)입니다.
- • LinkedHashSet은 순서 유지와 중복 제거를 동시에 제공
- • toCharArray()로 문자열을 문자 배열로 변환
- • StringBuilder를 사용하여 효율적인 문자열 결합
- • 시간복잡도 O(n)으로 효율적인 해결
만약 대소문자를 구분하지 않고 중복을 제거해야 한다면 어떻게 수정하시겠습니까?
Q. 학생 객체 리스트를 이름 오름차순으로 정렬하고, 이름이 같으면 나이 내림차순으로 정렬해야 합니다. Comparable과 Comparator 중 어떤 것을 사용해야 하며, 여러 정렬 조건을 어떻게 구현하시겠습니까?
이 경우 Comparator를 사용하는 것이 적합합니다. Comparable은 클래스의 기본 정렬 기준을 정의하지만, Comparator는 외부에서 다양한 정렬 기준을 유연하게 적용할 수 있습니다. Java 8 이상에서는 Comparator.comparing()과 thenComparing()을 체이닝하여 구현합니다. 예를 들어 Comparator.comparing(Student::getName).thenComparing(Comparator.comparing(Student::getAge).reversed()) 형태로 작성하며, Collections.sort() 또는 List.sort()에 전달하여 정렬합니다.
- • Comparator는 외부에서 정렬 기준을 정의하여 유연성 제공
- • Comparator.comparing()으로 첫 번째 정렬 조건 지정
- • thenComparing()으로 추가 정렬 조건 체이닝
- • reversed()로 내림차순 정렬 구현
Comparable을 구현한 클래스에서 Comparator를 추가로 사용하면 어떤 정렬 기준이 우선 적용됩니까?
Q. 파일을 읽어서 처리하는 메서드를 작성할 때, try-with-resources 구문을 사용해야 하는 이유와 일반 try-catch-finally 구문과의 차이점을 설명하고, 어떤 경우에 사용해야 하는지 말씀해주세요.
try-with-resources는 AutoCloseable 인터페이스를 구현한 자원을 자동으로 해제해주는 기능입니다. 일반 try-catch-finally에서는 finally 블록에서 명시적으로 close()를 호출해야 하며, 이때도 예외 처리가 필요해 코드가 복잡해집니다. try-with-resources는 try 블록이 종료되면 자동으로 자원을 닫아주며, 원본 예외와 close() 시 발생한 예외를 모두 보존하는 suppressed exception 기능을 제공합니다. FileInputStream, BufferedReader 등 파일이나 네트워크 자원을 다룰 때 필수적으로 사용해야 합니다.
- • AutoCloseable 구현 객체를 자동으로 해제
- • finally 블록 없이 간결한 코드 작성
- • suppressed exception으로 모든 예외 정보 보존
- • 파일, DB 연결, 네트워크 소켓 등 자원 관리에 필수
try-with-resources에서 여러 개의 자원을 동시에 열 때 자원이 닫히는 순서는 어떻게 됩니까?
Q. 정수 리스트에서 짝수만 필터링하고, 각 숫자를 제곱한 후, 합계를 구하는 작업을 Stream API로 구현하려고 합니다. 어떤 중간 연산과 최종 연산을 사용해야 하며, for문을 사용한 방식과 비교했을 때 Stream의 장단점을 설명해주세요.
Stream API에서는 filter()로 짝수를 필터링하고, map()으로 제곱 연산을 수행한 후, reduce() 또는 sum()으로 합계를 구합니다. 예를 들어 list.stream().filter(n -> n % 2 == 0).map(n -> n * n).reduce(0, Integer::sum) 형태입니다. Stream의 장점은 선언적 코드로 가독성이 높고, 병렬 처리가 쉬우며, 지연 연산으로 최적화가 가능하다는 점입니다. 단점은 작은 데이터셋에서는 for문보다 느릴 수 있고, 디버깅이 어려울 수 있습니다.
- • filter()로 조건에 맞는 요소 선택
- • map()으로 각 요소 변환
- • reduce() 또는 sum()으로 집계
- • 선언적 코드로 가독성 향상, 병렬 처리 용이
Stream의 중간 연산이 지연 평가(lazy evaluation)되는 것이 무엇이며, 이것이 성능에 어떤 영향을 미칩니까?
Q. 여러 스레드가 동시에 접근하는 카운터 클래스를 만들 때, int 변수를 사용하는 것과 AtomicInteger를 사용하는 것의 차이점을 설명하고, synchronized 키워드와 AtomicInteger의 성능 차이는 무엇인지 말씀해주세요.
일반 int 변수는 스레드 안전하지 않아 여러 스레드가 동시에 증가시키면 값이 유실될 수 있습니다. synchronized 키워드는 락을 사용하여 한 번에 하나의 스레드만 접근하도록 보장하지만, 락 획득과 해제 비용이 큽니다. AtomicInteger는 CAS(Compare-And-Swap) 알고리즘을 사용한 non-blocking 방식으로 락 없이 원자성을 보장합니다. 경합이 적을 때는 AtomicInteger가 synchronized보다 훨씬 빠르며, incrementAndGet(), addAndGet() 등의 메서드로 원자적 연산을 수행합니다.
- • 일반 int는 스레드 안전하지 않아 race condition 발생
- • synchronized는 락 기반으로 성능 오버헤드 존재
- • AtomicInteger는 CAS 알고리즘으로 non-blocking 동작
- • 경합이 적을 때 AtomicInteger가 성능상 유리
CAS 알고리즘이 실패했을 때는 어떻게 처리되며, 경합이 매우 심한 상황에서는 어떤 방식이 더 유리할까요?
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!