문자열, 예외, 제네릭
- String literal과 new String(””)의 차이를 설명해 주세요.
- String, StringBuilder, StringBuffer의 차이점에 대해서 설명해주세요.
- Exception과 Error의 차이는 무엇인가요?
- Exception 클래스의 예시를 말해주세요.
- Checked Exception과 Unchecked Exception의 차이는 무엇인가요?
- throw와 throws의 차이는 무엇인가요?
- try~catch~finally 구문에서 finally은 어떠한 역할을 하나요?
- Throwable과 Exception의 차이는 무엇인가요?
- 제네릭이란 무엇이고, 왜 사용할까요?
- 제네릭을 사용한 경험을 소개해 주세요.
람다, 스트림, 어노테이션, 리플렉션
- 람다란 무엇인가요?
- 스트림이란 무엇인가요?
- 람다와 스트림은 왜 생겨났을까요?
- 자바에서 어노테이션이란 무엇일까요?
- 어노테이션을 왜 사용할까요?
- 어노테이션은 리플렉션으로 동작한다고 말씀해 주셨는데, 리플렉션은 무엇인가요?
- 그러면 리플렉션을 활용해서 어노테이션의 메타 데이터를 가져오는 등의 로직을 실제로 구현해 보신 적이 있으신가요?
심화
뭔가.. 접은 글은 안예뻐보여서 그냥 나열하는 형식으로 작성한다..
답변
문자열, 예외, 제네릭
- String literal은 String Constant Pool에 저장된다. 동일한 문자열 리터럴이 여러번 사용되면, 자바는 메모리 절약을 위해 이미 있는 리터럴을 재사용함
- new 키워드를 사용하면 힙 메모리에 새로운 객체를 생성하지만, String Constant Pool에는 문자열 값이 저장되지 않는다
- String
1. 불변(immutable): 한 번 생성되면 그 값을 변경할 수 없다. 새로운 문자열로 변경하려면 새로운 객체를 생성해야됨
2. 성능(spec): 문자열 변경 작업이 적고, 읽기를 주로 할 때 빠름
3. 사용(usage): 문자열을 변경할 필요가 없고, 문자열 리터럴을 자주 사용할 때 유리
- StringBuilder
1. 가변(mutable): 문자열을 수정할 수 있음. 내부적으로 배열을 사용해 효율적으로 문자열을 조작함
2. 성능(spec): 동기화(synchronization)되지 않기 때문에 멀티스레드 환경에서 안전하지 않음. 동기화가 필요 없는 환경에서 가장 빠름
3. 사용(usage): 문자열을 자주 수정해야 하며, 멀티스레드 환경이 아닐 때 유리
- StringBuffer
1. 가변(mutable): 문자열을 수정할 수 있음. 내부적으로 배열을 사용해 효율적으로 문자열을 조작함
2. 성능(sepc): 동기화(synchronization)되어 있어 멀티스레드 환경에서 안전. 대신 동기화로 인해 StringBuilder보다 느림
3. 사용(usage): 멀티스레드 환경에서 문자열을 자주 수행할 때 유리
- Error
- 시스템 수준에서 발생하는 심각한 문제로, 복구하기 어렵고 대부분 처리하지 않음
- ex) OutOfMemory, StackOverflow
- Exception
- 프로그램에서 예측 가능한 문제로, 프로그래머가 적절히 처리하여 프로그램의 비정상 종료를 막을 수 있음
그럼, 추가 질문이 있을 수 있다
- OOM은 개발자의 실수나 부주의로 인해서 발생할 수 있는 문제인데 왜 Exception이 아니라 Error로 치부될까?
이유는 1. 프로그램이 지속적으로 실행될 수 없는 상태 2. 복구의 어려움 3. 시스템 수준 문제
NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException, IOException, SQLException, ClassNotFoundException
Throwable
├── Error
└── Exception
├── RuntimeException (Unchecked Exception)
└── Checked Exceptions (모든 RuntimeException을 제외한 예외들)
Exception은 Checked/Unchecked로 구분될 수 있고, Unchecked Exception은 RuntimeExcpetion의 하위 클래스들을 의미한다
- Checked Exception( != RuntimeException)
- 컴파일 시점에서 반드시 처리해야 하는 예외 -> 처리하지 않으면 컴파일 오류가 발생
- RuntimeExcpetion을 상속받지 않음
- ex) IOException, SQLException, ClassNotFoundException
- Unchecked Exception ( == RuntimeException)
- 런타임 시에만 발생할 수 있는 예외 -> 주로 논리적 오류로 발생 -> 필요에 따라 처리하거나 하지 않음
- RuntimeExcpetion을 상속받음
- ex) Unchecked Excpetion: NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException
- throw: 예외를 발생시키기 위해 사용. 프로그래머가 특정 조건이 발생했을 때 직접 예외를 발생시킬 수 있음
- throws: 메서드 시그니처에서 사용되어 해당 메서드가 호출될 때 어떤 예외가 발생할 수 있는지를 선언함. 즉 이 메서드가 XX라는 예외를 던질 수 있다는 것을 명시하는 데 사용
// Throw 예시
if (num < 0) {
throw new IllegalArgumentException("Number must be positive");
}
--------------
// Throws 예시
public void readFile(String fileName) throws IOException {
...
}
심화질문1) 개발하다보면 어떤 예외는 new throw Ex()를 하면 메서드에 throws를 명시해줘야하고, 어떤 예외는 그렇지 않은데.. 이 두개 차이점을 알고있나?
- 아마도 new throw Ex()를 하고 메서드에 throws를 해줘야 하는 예외는 Checked Exception일 것이다. 이 예외들은 컴파일 타임 이전에 반드시 처리되어야 하는 예외들이다. 처리 방법은 try-catch를 하거나 상위 메서드로 던질 수 있다
-> 여기에서 상위 메서드로 던진다는 표현보다 좀 더 우아한 표현으로 상위 메서드로 책임을 위임한다라고 표현해보자
- 그렇지 않은 예외는 unChecked Exception으로서, RuntimeException을 상속받는 클래스 예외인 경우이다. 언체크 예외는 프로그램의 논리적 오류를 나타낸다고 했다
ex) NullPointerException, RuntimeException..
보통 스프링으로 개발할때는 비즈니스레벨에서 발생할 수 있는(예측할 수 있는) 예외들을 RuntimeException을 상속받아서 만들고, ControllerAdvice(RestControllerAdvice)에서 catch해서 REST json 응답으로 내려준다
예외 발생 여부와 상관없이 항상 실행되는 코드를 포함한다
이 블록은 자원을 해제하거나, 파일을 닫는 등 어떤 일이 있더라도 반드시 실행되어야 하는 작업을 처리할 때 유용하다
또한 로그 작성, 상태 리셋 등을 finally에 구현하기도 한다
- try 구문이 catch로 빠지던(실패하던), 빠지지 않던(성공하던) 상관 없이 finally구문이 항상 실행되는 모습을 볼 수 있다
참고로 try-with-resources라는 기법(문법)도 있는데 try 구문 안에 ( ) Argument로 AutoCloseable를 구현한 자원들이 try 블록이 끝나면 자동으로 자원을 닫게 해주는 기능을 제공한다(자바7부터 나왔음)
명시적으로 맨날 try { ... }만 사용하지 말고 try(...) { ... }를 고려해보자(자원반납이 필요한 경우)
- 자원: 파일(FileInputStream), 네트워크(소켓), 데이터베이스 연결, 메모리(byte buffer), 쓰레드 풀 등
=> BufferReader를 따라가봤다
내부적으로 Reader를 상속받고 있었고
Reader는 Readable, Closeable이란 인터페이스를 구현하고 있었다
여기에서 AutoCloseable 인터페이스를 상속받는 Closeable 인터페이스를 볼 수 있었다
이 객체를 살펴봄으로써 단순 암기식으로 외우는 분들에게 드릴 팁도 챙겼다
extends -> 상속. 클래스끼리 사용
implements -> 구현. 인터페이스를 구현할때 사용
맞을까?
인터페이스는 다중상속이 된다고 했다
인터페이스끼리는 extends 키워드로 상속받아서 사용하게 된다는 것을 알 수 있었다
public interface InterfaceA {
void methodA();
}
public interface InterfaceB extends InterfaceA {
void methodB();
}
- Throwable: 자바에서 예외나 오류가 발생했을 때 throw될 수 있는 객체의 최상의 클래스이며 Error, Exception 하위 클래스를 포함
- Exception: Throwable의 하위 클래스이면서 프로그램에서 발생할 수 있는 예외 상황을 나타내며 check/uncheck 예외로 나뉜다
* Hierarchy 보는법: MacOS기준 ctrl + H
* Diagram 보는법: MacOS 기준 Shift + opt + cmd + U
예상할 수 있는 추가질문) Throwable의 설명을 보면 자바에서 예외나 오류가 발생했을 때 throw될 수 있는이라고 설명했는데, Error는 시스템적인 오류를 나타내고 일반적으로 핸들링 하지 않는다고 했다. 근데 어떻게 throw를 한다는거지? 에 대해서 의문을 품고 질문할 수 있다
결론만 말하면, 사실 Error도 throw 할 수 있다
하지만 Error를 throw하는 것은 일반적으로 권장되지 않는다.
Error는 시스템의 심각한 문제를 나타내므로, 직접 처리하거나 throw하는 것이 아니라, 발생했을 때 시스템이 이를 감지하고 종료되도록 설계된 것이기 때문이다
내가 질문 또는 프로그래밍적으로 어려움을 마주했을 때 맨 처음 하는것: 용어 뜻 찾기
자, 제네릭을 검색해보자. 스펠링은 Generic 이다
일반적인, 총칭적인 < 이라는 뜻으로 설명할 수 있을 것 같다
설명과 사용하는 이유는 chatgpt가 말해준 내용을 대신하고, 다음 질문의 제네릭을 사용한 경험에서 사용 사례로 더 풀어본다
제네릭(Generic)이란 컴퓨터 프로그래밍에서 코드의 재사용성을 높이기 위해 사용하는 기법 중 하나입니다. 제네릭은 타입을 일반화하여 특정 데이터 타입에 종속되지 않는 코드를 작성할 수 있게 해줍니다. 이를 통해 동일한 알고리즘이나 자료구조를 다양한 데이터 타입에서 사용할 수 있습니다.
예를 들어, 자바(Java)에서의 제네릭은 클래스나 메서드가 사용할 데이터 타입을 명시하지 않고, 코드가 실행될 때 사용할 타입을 지정할 수 있게 합니다. 이런 방식으로 타입 안전성을 확보하고, 코드의 중복을 줄이며, 더 유연한 코드를 작성할 수 있습니다.
왜 제네릭을 사용하는가?
- 코드 재사용성
- 타입 안정성
- 유연성
- 가독성 향상
이라고 말해줬는데.. 깨알팁으로 신입개발자 기준에서 코드 재사용성, 유연성, 가독성 향상이라는 키워드를 이력서에 남발하면 deep한 질문으로 공격받기 쉬움으로 최소한으로 말하자
사실상 스프링 강의를 배웠거나, 스프링로 프로젝트를 해봤다면 자기도 모르게 제네릭을 마음껏 쓰고 있는거다
제네릭은 <T> 타입으로 쓰는거기때문에 잘 기억을 떠올려보자
- Spring Data Jpa를 사용하며 Repository에서 사용
- ResponseBody대신 ResponseEntity를 구현하며 사용
- Convertor를 구현할 때 사용
- 타입 안정성 보장
- 메서드 호출부와 정의부를 다르게 사용(이건 다형성도 포함해서) - 정의를 제네릭으로, 호출부는 다형성으로
1번
자바 면접질문이긴한데.. 스프링과 JPA를 공부해본 사람이 있다면 CrudRepository vs JpaRepository의 차이점도 알아두자
2번
컨트롤러에서 ResponseEntity로 Response타입을 담아서 Return해줬다. Status code나 URI Location을 동적으로 조절할 수 있는 장점이 있다. ResponseEntity에 대해서 모른다면 ResponseEntity vs ResponseBody 에 대해서도 알아두면 좋다
3번
파라미터로 asc, desc가 넘어오면 api가 작동하지 않길래 ASC, DESC로 변환될 수 있도록 해주는 코드
예전에는 JPA와 Postgresql의 UUID[] 타입이 맞질 않아서 작성한 경우도 있었다
4번
이번 예시는 내가 첫 회사를 취업하고 눈으로 봤던 코드다... 맨 처음 프로젝트가 전자정부프레임워크로 되어있고, 자바6인가 7을 쓰고 있었다..ㅋ..으악!!!! 다시는 떠올리기 싫어
실행이 잘 되지? 하지만 강제형변환이 필요한 경우라면?
이런거는 코드를 작성할 때는(Compile time) 발견할 수 없고, Run Time 시에만 발견할 수 있는 경우이다(위에서 답한 Error가 아닌 Exception)
어차피 좋은 IDE인 IntelliJ를 사용하면 알아서 조언을 해준다
해석하면 너 지금 ArrayList를 날것으로 쓰고 있어. 그러니 generic을 추가해보지 않으련? 라고 말하고 있다
그럼 위와같은 실수를 방지하기 위해서는 코드를 바꿔야 한다
제네릭 타입에 명확한 타입을 사용해서 컴파일 되기 이전에 예외를 발견할 수 있다
제네릭을 사용하는 경우는 아니라 이것과 완전히 같은 문제는 아니지만, 자바/코틀린같이 컴파일 타임이 있는 언어를 사용하는 개발자들은 런타임이 아니라 컴파일 타임에 에러를 잡고 싶어한다. 이처럼 예외를 미리 발견할 수 있기 때문이다
대표적인 예로 QueryDSL 사용이다. 문자열로 쿼리를 날리다 보면 내가 미처 발견하지 못한 오타(typo)등을 컴파일 타임에 잡아줄 수 있게 하는 고마운 도구이다
5번
ArrayList와 LinkedList는 List 인터페이스를 구현해서 사용하는 구현체이다
즉, 부모타입으로도 사용할 수 있음 -> 다형성
추가로 list에 들어가는 element들의 타입에 영향을 받지 않도록 T로 명시해줘서 제네릭 함수로 만들어서 사용했다 -> 제네릭
람다, 스트림, 어노테이션, 리플렉션
람다(lambda)는 익명 함수(anonymous function)를 의미한다
이름이 없는 함수로, 보통 일회성으로 사용되며, 간단한 연산이나 표현식에 사용된다
추가로 덧붙여서, AWS lambda의 이름 뜻을 생각해보면 람다는 1회성으로 함수가 실행되기때문에 AWS 람다 또한 특정 작업을 수행하기 위해 일시적으로 사용된 후, 더 이상 유지되지 않고 사라지는 특성을 갖고 있기 때문에 AWS 람다함수라고 불린다!!
(int x, int y) -> x + y
자바에서는 위와 같은 syntax로 사용 가능
밑에 추가 면접 질문들에서 람다에 대해서 더 깊게 물어보지 않기에... 내가 아는 지식을 또한 덧붙여본다
자바에서는 람다를 함수 시그니처로 사용할 수 없다. 이유는 함수 자체를 일급 객체로 다루지 않기 때문. 대신 함수형 인터페이스(추상 메서드가 하나만 있는 인터페이스)를 통해 람다를 수용할 수 잇는 함수 시그니처를 제공한다
아래는 함수형 인터페이스를 사용하는 예시와 내가 직접 만들어서 사용할 수 있는 예시이다
코틀린에서는 함수를 일급 객체로 취급하기 때문에 람다를 함수 시그니처로 사용할 수 있다
- 데이터의 흐름을 추상화한 개념
- 대량의 데이터를 표준화된 방식으로 처리하기 위한 용도로 설계됨
- 컬렉션(리스트, 셋 등)의 데이터를 다룰 때 사용됨
- 스트림은 데이터를 필터링(filter), 변환(map), 정렬(sorted), 집계(reduce) 등의 연산을 수행하는 데 사용된다(Stream API)
- 람다
1. 간결한 코드: 익명 클래스를 정의할 필요 없이 간단하게 함수형 프로그래밍 구현 가능
2. 가독성 향상: 코드의 목적이 명확해지고, 불필요하게 반복되는 보일러플레이트 코드를 줄일 수 있음
3. 함수형 프로그래밍 지원: 함수를 일급 객체로 다루게 하며, 고차 함수같은 함수형 프로그래밍 패러다임 적용 가능
4. 콜백 함수 구현: 비동기나 이벤트 기반 프로그래밍에서 콜백 함수를 간결하게 구현할 수 있음
- 스트림
1. 데이터 처리의 간결성: 스트림 API를 사용하면 데이터의 필터링, 매핑, 집계 등을 단순한 선언형 코드로 표현할 수 있
2. 지연 연산 지원: 스트림은 필요할 때만 데이터를 처리하는 지연 연산을 지원. 이를 통해 불필요한 연산을 줄이고 성능 최적화 가능
3. 병렬 처리 지원: parallelStream() 메서드를 통해 멀티코어 환경에서의 성능을 크게 향상시킬 수 있음
4. 데이터 파이프라인(체이닝): filter(), map(), reduce() 같은 작업을 체인으로 연결해서 데이터 파이프라인을 구성하여 복잡한 데이터 처리 작업을 순차적으로 수행할 수 있게 지원함
- 람다 + 스트림
람다와 스트림을 사용하면 함수들이 주로 순수 함수로 구성되어 사이드 이펙트가 발생하지 않음
결론적으로 코드의 예측 가능성을 높이고, 사이드 이펙트를 방지하여 코드의 안정성과 병렬 처리의 안전성을 보장하게 됨
[ 순수함수: 동일한 입력에 대해 항상 동일한 출력을 반환하며, 함수의 실행이 외부 상태에 영향을 미치지 않음 ]
[ 부작용(Side Effect): 함수 내부에서 외부 변수를 변경하거나, 외부 변수에 의존하지 않음 ]
- Annotation은 메타데이터의 일종으로, 코드에 추가적인 정보를 제공하는데 사용
- 클래스, 메서드, 필드, 매개변수 등 여러 요소에 붙일 수 있음
- 주로 컴파일러에 의해 처리되거나 런타임에 특정 동작을 수행하는 데 사용
- syntax) @ 기호로 시작하며, 예를 들어 @Override, @Deprecated, @SuppressWarnings 등이 있음 (Spring에선 @Autowired)
- 코드 가독성 향상: 어노테이션을 사용하면 코드에 대한 부가적인 정보를 명시적으로 표시(ex: @Deprecated: 더이상 사용하지 않는 메서드임을 메타데이터로 알려주는 경우)
- 컴파일러 지원: 컴파일러는 어노테이션을 통해 특정 규칙을 검사하거나 경고를 표시할 수 있음(ex: @Override 메서드가 상위 클래스의 메서드를 올바르게 오버라이드했는지 검사)
- 런타임 처리: 런타임에 리플렉션(Reflection)을 통해 읽혀져서 특정 동작을 수행할 수 있음(ex: @Entity 어노테이션을 붙인 클래스는 JPA(Java Persistence API)에 의해 데이터베이스 테이블로 매핑됨)
- 프레임워크와의 통합: 자바 프레임워크(Spring, Spring boot)에서 annotation을 통해 설정을 간편하게 하거나, 특정 동작을 정의함
리플렉션(Reflection)은 자바 프로그래밍 언어에서 프로그램이 런타임에 자신의 구조를 검사하고 수정할 수 있는 기능을 제공
리플렉션으로 가능한 기능
- 클래스 정보 얻기: 클래스의 이름, 메서드, 필드, 생성자, 어노테이션 등의 메타데이터를 런타임에 얻을 수 있음
- 객체 생성: 클래스를 동적으로 로드하고, 그 클래스의 인스턴스를 생성할 수 있음
- 메서드 호출: 런타임에 메서드 이름을 알고 있거나 동적으로 결정하여 해당 메서드를 호출할 수 있음
- 필드 접근: 객체의 필드를 동적으로 읽거나 수정할 수 있음(private 접근에 상관없이 필드에 접근 가능)
- 어노테이션 처리: 클래스나 메서드에 선언된 어노테이션 정보를 읽고 그에 따라 동적으로 동작을 결정할 수 있음
Q. 그럼 이 모든 것들은 어떤 코드를 사용하는 걸까? (내가 만든 꼬리질문)
일부러 질문을 헷갈리게 해봤다. 사실 패키지에 있는 클래스, 인터페이스 등 구현체들을 갖다 쓰는거다
java.lang.reflect < 이 키워드만 말하면 된다
Q. 자바에서 Reflection이 가능한 이유는??
자바(정확히는 JVM위에서 도는 모든 언어들)에서 Reflection이 가능한 이유는 Java가 JVM위에서 동작하기 때문이다
JVM은 자바 바이트코드(.class)를 실행하는 가상 머신으로서 프로그램이 실행되는 동안 메모리에 로드된 클래스의 메타데이터, 메서드, 필드 등의 정보를 유지하고 관리한다
이 JVM에서 클래스 로딩/메타데이터 유지, 동적 바인딩(dynamic binding)을 지원하기 때문이다
넵. 테스트코드를 작성할때 protected 접근제한자 프로퍼티를 갖고 있는 Jpa 엔티티를 테스트할때 사용해봤습니다 < 내 대답
여기서 당당하게 AOP로 Reflection을 사용했습니다! < 라고 말하면 AOP 질문들에 의해서 썰릴 수 있다
AOP가 내부적으로 Reflection을 사용할 수는 있으나, 코어개념은 Proxy Pattern을 사용했다는 점이다
출처
https://docs.spring.io/spring-framework/reference/core/aop.html
https://docs.spring.io/spring-framework/docs/2.5.x/reference/aop.html
https://velog.io/@18k7102dy/Spring-AOP-Spring-AOP%EB%A5%BC-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EC%A3%BC%EC%9D%98%ED%95%B4%EC%95%BC%ED%95%A0-%EC%A0%90
심화
뭐 마냥 나쁜건 아니다. 프로덕션 코드로 남기면 안좋다는거지..ㅎ
아직도 디버깅할때 break point를 쓸 줄 모르거나 log를 안써봤거나, log 레벨별 차이를 모른다고 하면 쓸수도 있는거고
하지만 배포해서 서비스가 되어야 하는 프로덕트에서 println으로 로깅을 하고 있다는 건 성능문제로 이어질 수 있다
성능 이슈를 일으킬 수 있는 문제점들
1. 동기화 비용
2. I/O 작업의 본질적인 속도(콘솔 출력)
3. 버퍼링 문제
4. 비동기 대안 부재
synchronized를 사용하면 멀티스레드 환경에서는 안전하지만(데이터 무결성 보장), 동기화 비용이 생겨서 오버헤드가 발생하게 된다
+ 추가질문
Q. 그럼 만약에 println이 아닌 logger를 사용하면 모두 다 비동기로 동작하기때문에 All Happy해질까?
A. 그렇지 않다. logging framework에서 비동기로 로그를 찍는 appender를 xml로 정의하거나, 코드에서 조작해야된다
결론: 만약 회사에 dev/staging 서버가 있다면 print로 디버깅해도 상관없지만(그래도 로그 찍자), prod 서버에서는 logging 프레임워크를 사용하자
'Course > JSCODE - 자바' 카테고리의 다른 글
3주차 스터디 노트 (2) | 2024.08.29 |
---|---|
1주차 스터디 노트 (0) | 2024.08.16 |
JSCODE 스터디 신청 (0) | 2024.08.16 |
댓글