[면접 스터디용 질문 리스트] JAVA/SPRING/JPA/Redis/ 면접 질문 리스트 10개 씩
JAVA
객체지향원칙의 OOP
와 SOLID
에 대해 예시와 함께 설명해주세요
답변 :
- 캡슐화
- 객체의 데이터(필드)를 직접 접근하지 못하게 하고, 메서드를 통해서만 접근할 수 있도록 보호하는 개념입니다. 데이터를 보호하고 무결성을 유지할 수 있습니다. 이 방식 뿐만 아니라 접근 제한자, 인터페이스를 사용하여 필드, 메서드, 클래스를 캡슐화 하는 방법도 존재합니다.
-> 그럼 접근 제한자로는 어떻게 캡슐화를 할 수 있죠?
접근 제한자는 네 가지로 public, protected, default, private로 나눌 수 있습니다. public은 모든 패키지에서 임포트하여 사용할 수 있습니다. protected는 자신이 속한 패키지와 클래스를 상속받은 클래스에서 사용 가능합니다. package는 자신이 속한 패키지에서만 사용가능하고 private는 자기 자신 클래스에서만 사용이 가능합니다.
클래스단위는 Public과 default를 적용할 수 있고 필드와 메서드는 모두 적용 가능합니다.
-> 인터페이스로는 어떻게 캡슐화를 할 수 있나요?
인터페이스로 상속받은 클래스를 사용하는 사용자 입장에서는 구현체의 내부 로직을 모르고 사용할 수 있으며, 알 수 있는 방법이 없기에 캡슐화를 했다고 말할 수 있습니다.
- 상속
기존 클래스를 확장하여 새로운 클래스를 생성하는 개념으로 기존 기능을 재사용하면서도 필요한 부분만 변경 가능한 장점이 있습니다. - 다형성
하나의 메서드나 객체가 여러 형태를 가질 수 있도록 하는 개념입니다. 예시로 오버로딩과, 오버라이딩을 말씀드릴 수 있습니다.
-> 오버로딩, 오버라이딩을 추가로 설명해주세요
오버 로딩은 동일한 메서드명을 사용하지만 매개변수의 객체의 타입에 따라 동작이 달라지는 특징이 있습니다.
오버라이딩은 상속받은 클래스의 메서드를 가져와 재정의한다는 특징이 있습니다. 선언부와 구현부가 모두 동일해야 하며 내부 로직만 변경됩니다.
- 추상화
필요한 세부정보를 숨기고 필요한 정보만 외부에 제공하는 개념입니다. 이는 캡슐화와 의미가 유사한데 사용자는 내부 구현을 몰라도 기능을 사용할 수 있습니다.
S
OLIDSingle Responsibility Principle
로 클래스는 하나의 책임만 갖고, 클래스를 수정하는데에 있어도 하나의 목적을 가지고 수정해야 함을 말합니다.- S
O
LIDOpen/Closed Principle
로 확장은 가능하지만, 기존 코드를 수정하지 않아야 합니다. 예를 들어 기존의 기능에 대한 개선이 이뤄진다고 하면 기존 코드에 대해서 수정이 이뤄지는것이 아닌 새로운 기능을 추가하여 확장할 수 있도록 합니다. - SO
L
IDLiskov Substitution Principle
원칙은 부모 클래스의 객체를 자식 클래스로 대체해도 정상적으로 동작해야 함을 말합니다. 예를 들어 새라는 객체가 날다 라는 메서드를 갖고 있는데 이를 상속받은 펭귄은 이 메서드를 수행할 수 없기에 치환원칙에 위배된다고 볼 수 있습니다. - SOL
I
DÌnterface Segregation Principle
로 클라이언트가 사용하지 않는 인터페이스를 강요하면 안된다는 원칙입니다. 예를 들어 최상위에 핸드폰이 있는데 여기에 시리와 빅스비가 함께 있고 이를 삼성과 아이폰 객체가 상속받는다면 생기는 문제입니다. 각 인터페이스를 분리하는 것을 고려해야 함을 볼 수 있습니다. - SOLI
D
Dependency inversion Principle
로 상위 모듈이 하위 모듈에 의존하면 안되고, 추상화에 의존해야 한다는 원칙입니다. 가장 추상적인 객체를 상속받고 사용하면 인터페이스의 구현체를 교체하여 사용하여도 영향을 미치지 않는다는 원리를 말하고 있습니다.
Java의 컴파일 과정에 대해 설명해주세요
자바는 "Write Once, Run Anywhere" 이라는 철학을 기반으로 소스코드 -> 바이트 코드 -> 실행 과정을 거칩니다.
개발자가 작성한 코드는 javaC(자바 컴파일러)가 바이트 코드로 변환하고 이를 JVM에서 로드하여 메모리에 적재합니다.
JVM이 적재된 바이트 코드를 해석하거나, JIT 컴파일러가 네이티브 코드로 변환하여 실행합니다. 처음에는 바이트 코드를 인터프리팅하지만 자주 실행되는 코드를 네이티브 코드로 변환하여 성능을 최적화합니다.
-> 클래스 로드 단계를 더 자세히 설명해주세요
클래스 로더가 .class 파일을 찾아서 메모리에 로드합니다. 바이트 코드는 JVCM의 메서드 영역에 저장됩니다. 클래스의 static 변수, 메서드 정보등을 저장합니다. 로딩이 끝나면 class 객체가 생성되며 이를 통해 리플렉션이 사용 가능합니다.
Class<?> clazz = Class.forName("com.example.MyClass");
그 다음은 링크입니다. .class 파일이 유효한 바이트 코드인지 검사하고 잘못된 코드가 실행되지 않도록 방지합니다. 이상이 없다면 static 변수의 메모리 할당 및 기본값으로 초기화 합니다. 그 후 클래수 내부의 심볼릭 레퍼런스를 실제 메모리 주소로 변경합니다.
예를 들어 System.out.println()에서 System.out이 어디에 있는지 메모리 주소를 찾습니다.
그 다음은 초기화 입니다. static 변수에 명시된 값 할당 및 static{} 블록을 실행합니다. 클래스가 사용 가능한 상태가 됩니다.
- 클래스 로딩 과정은 로딩(Loading) → 링크(Linking) → 초기화(Initialization) 3단계로 진행된다.
- 로딩 단계에서 .class 파일을 메모리에 올리고, 링크 단계에서 바이트 코드 검증 및 static 변수 기본값 할당이 이루어진다.
- 초기화 단계에서는 static 변수 값이 설정되고 static {} 블록이 실행되며, 이후 클래스가 실제 사용될 준비가 완료된다.
Java에서 main메서드가 static인데 JVM의 저장 영역과 관련해서 설명해주세요
JVM의 메모리 구조는 메서드, 힙, 스택, PC레지스터, 네이티브 메서드 스택 영역이 있습니다.
메서드 영역은 클래스 정보, static 변수, static 메서드 저장이 이루어집니다.
힙 영역은 객체와 인스턴스 변수가 저장됩니다.
스택 영역은 각 스레드의 메서드 호출 스택(참조 변수)과 지역 변수가 저장됩니다.
PC 레지스터는 현재 실행중인 JVM 명령어의 주소를 저장합니다.
네이티브 메서드 스택은 C,C++등의 네이티브 코드 실행 시 사용됩니다.
main() 메서드는 static이므로 객체를 생성하지 않고도 메서드 영역에 로드되어 실행 가능합니다.
main()이 실행되면 스택 프레임이 생성되며, 객체는 힙에 저장되고 참조 변수는 스택에 저장됩니다.
main에서 수행되는 메서드들은 새로운 스택 프레임으로 main뒤에 쌓여 수행이 완료되면 pop된다. 지역 변수들은 지역변수 배열에 저장되고 연산되는 값의 자료는 operand Stack에 저장된다.
자바는 사용하지 않는 객체를 어떻게 처리하나요?
Java는 가비지 컬렉터를 사용하여 더이상 사용되지 않는 객체를 자동으로 제거합니다. 프로그래머가 직접 해제할 필요 없이 필요없는 객체를 감지하고 회수합니다.
GC는 영역이 나뉘어져 있는데
Young Generation , Old Generation으로 나뉩니다. YG에서는 Minor GC가 수행되는데 Eden영역이 가득 차면 실행됩니다. Eden영역은 객체가 처음 할당되는 공간입니다. 여기서 참조되지 않는 객체들은 제거되고 살아남은 객체는 Survivor 영역으로 이동합니다. 여러 번 살아남는 객체는 OG로 이동합니다.
다음은 OG에서 Major GC가 발생합니다. 많은 객체가 한 꺼번에 정리되기 때문에 성능에 영향을 줄 수 있습니다.
-> Survivor가 어떻게 꽉 찼음을 감지하는지 설명해주세요
Eden에서 살아남은 객체를 현재 비어있는 Survivor0 or 1로 이동합니다. 이때 객체의 생존 횟수가 1씩 증가합니다. 기본적으로 15회 이상 살아남으면 OG로 이동합니다. 하지만 Survivor에 공간이 부족하다면 age가 충분하지 않더라도 일부 객체를 보낼 수 있습니다.
만약 Survivor 공간이 부족하면 일부 객체를 OG로 승격시킵니다.
추상클래스와 인터페이스의 차이점을 설명해주세요
추상 클래스는 단일 상속만 지원하며 구현된 메서드와 추상 메서드를 혼합해서 가질 수 있습니다. 상속받는 클래스는 추상 메서드의 미구현 메서드를 구현해야 합니다.
인터페이스는 모든 메서드가 기본적으로 추상 메서드이며, 기본 구현도 할 수 있습니다.상속받는 클래스는 인터페이스의 모든 메서드를 구현해야 합니다.
-> 그럼 두 가지는 어떤 목적으로 사용될 수 있나요?
추상 클래스는 공통된 기본 구현을 제공하고 싶을 때 사용합니다. 상속 관계가 명확하고 기본적인 구현을 제공하면서 상속받은 클래스들이 추가적으로 세부 구현을 할 때 적합합니다.
인터페이스는 다양한 클래스들이 공통된 메서드 시그니처를 구현해야 할 때 사용합니다. 객체의 행위만 지정하고, 구현은 각 클래스에서 하도록 합니다.
ArrayList와 LinkedList의 차이점
ArrayList는 배열을 기반으로 데이터를 저장합니다. 고정된 배열을 사용하여 저장된 데이터에 인덱스를 통해 빠르게 접근 가능합니다. 배열이 꽉 차면 새로운 배열을 할당하고 기존 데이터를 복사하여 크기를 확장합니다. ArrayList보다 검색에서는 빠르지만 삽입/삭제에서는 O(n)의 시간이 걸립니다.
LinkedList는 각 요소에 노드와 데이터로 구성되어 있고, 각 노드는 데이터와 다음 노드를 가리키는 포인터를 포함합니다. 데이터는 순차적으로 연결되어 있기 때문에 인덱스를 통한 빠른 접근은 어렵습니다. 하지만 삽입/삭제에서는 노드 간의 링크만 변경하면 되기 때문에, 중간에 삽입/삭제가 매우 빠릅니다.
- CheckedException과 UnCheckedException의 차이는?
CheckedException은 컴파일 시 발생할 수 있는 예외를 의미합니다. 파일 입출력, 네트워크 연결등에서 발생할 수 있는 예외들이 해당되며 이는 throws로 전가처리를 해주거나 try-catch블록으로 처리해야 합니다.
UnCkeckedException은 런타임시에 발생할 수 있는 예외이며 NullPionterException, ArrayIndexOutOfBounds 등이 대표적인 예시입니다. 예외처리가 강제되지 않지만 발생 시 프로그램이 종료될 수 있습니다. 주로 프로그래밍 오류를 말합니다.
Object에서 equals와 hashCode를 재정의하는 이유는 뭘까요?
Object에서 equals와 hashCode는 참조 비교만 수행하기 때문에 값 비교를 위해서는 반드시 재정의해야 합니다. hashCode를 함께 정의하는 이유는 해시 기반 컬렉션에서 같은 객체를 찾아야 하는 상황이 생기기도 하기 때문입니다.
-> 동일성과 동등성에 대해서 정리해주실래요?
동일성은 두 객체가 물리적으로 동일한 객체인지, 메모리 주소가 같은지 비교하는 개념입니다. == 을 사용합니다.
동등성은 두 객체의 값이 같으면 동일한 내용을 가지고 있다고 비교하는 개념입니다. equals를 사용합니다.
- String, StringBuffer, StringBuilder의 차이는?
String은 불변 클래스로 한 번 생성되면 변경될 수 없으며 새로운 값을 할당하려면 새로운 객체를 생성해야 합니다.
StringBuffer는 가변 클래스입니다. 문자열을 변경할 수 있으며, 멀티스레딩 환경에서 안전한 동기화를 제공합니다.
StringBuilder는 StringBuffer와 기능은 동일하지만 동기화가 없고 단일 스레드 환경에서 더 성능이 좋습니다. - Generic이 무엇이고 왜 사용하는걸까요
Generic은 타입 매개변수를 사용하여 클래스, 인터페이스, 메서드가 다양한 타입을 처리할 수 있게 해주는 자바의 기능입니다.
자바에서 제네릭을 사용하면 컴파일 시점에서 타입을 체크해주어 타입 안정성을 제공하고, 형 변환에 의한 런타임 오류를 예방할 수 있습니다.
자바에서 동시성을 해결 할 수 있는 방법이 어떤게 있나요
Thread를 사용하여 직접 스레드를 생성하고 실행하는 방법으로 각 스레드는 독립적으로 실행되며 하나의 프로그램내에서 병렬적으로 처리될 수 있습니다.
Runnable 인터페이스 구현 방법이 있습니다. Runnable인터페이스를 구현하여 스레드를 실행할 작업을 정의합니다. 이를 통해 스레드 풀등을 활용할 수 있습니다.
ExecutorService는 스레드 풀을 사용하여 여러 스레드를 효율적으로 관리합니다. 스레드 풀을 사용하면 자원을 미리 생성하고 재사용하여 성능을 최적화할 수 있습니다.
synchronized 키워드를 사용하여 여러 스레드가 공유 자원에 접근할 때 동기화 처리를 할 수 있습니다. 이는 상호 배제(Mutual Exclusion)를 통해 자원 충돌을 방지합니다.
ReentrantLock은 synchronized보다 더 세밀한 동기화 제어가 가능합니다. 락을 수동으로 획득하고 해제할 수 있기 때문에, 교착 상태를 예방할 수 있는 기능이 있습니다.
Atomic 클래스는 원자성을 보장하며, 공유 자원에 대해 동기화 없이 안전하게 값을 변경할 수 있습니다. AtomicInteger, AtomicLong 등이 있으며, CAS(Compare-And-Swap) 방식으로 동작합니다.
Concurrent Collections는 멀티스레드 환경에서 안전하게 사용할 수 있는 컬렉션들을 제공합니다. 예를 들어, ConcurrentHashMap이나 CopyOnWriteArrayList 등이 있습니다.
Spring
SpirngMVC의 동작구조에 대해서 설명해보세요
Spring MVC는 웹 애플리케이션을 개발하는 데 사용되는 모델-뷰-컨트롤러(Model-View-Controller) 디자인 패턴을 따르는 프레임워크입니다. 이 구조는 애플리케이션을 세 가지 주요 컴포넌트로 나누어, 유지보수성을 높이고, 구조적으로 더 효율적인 코드를 작성할 수 있도록 합니다.
- Model: 애플리케이션의 비즈니스 로직과 데이터, 즉 실제로 처리할 데이터를 담고 있는 객체.
- View: 사용자에게 데이터를 표시하는 역할을 하며, HTML, JSP, Thymeleaf 등으로 구현됩니다.
- Controller: 사용자 요청을 처리하고, 모델과 뷰를 연결하여 결과를 생성하는 컴포넌트.
Spring MVC는 클라이언트의 요청을 처리하고 응답을 반환하는 일련의 과정에서 DispatcherServlet을 중심으로 여러 컴포넌트들이 협력하는 방식으로 동작합니다.
1. 클라이언트 요청:
• 사용자가 웹 애플리케이션에 HTTP 요청을 보냅니다. 예를 들어, 브라우저에서 URL을 요청합니다.
2. DispatcherServlet:
• DispatcherServlet은 Spring MVC의 핵심입니다. 모든 요청은 DispatcherServlet을 통해 들어옵니다. 이는 Front Controller로서, 클라이언트의 요청을 받아 핸들러 매핑을 통해 적절한 Controller를 찾아 요청을 전달합니다.
3. HandlerMapping:
• DispatcherServlet은 HandlerMapping을 통해 요청 URL에 매핑된 컨트롤러를 찾습니다. 이를 통해 어떤 컨트롤러의 메서드가 요청을 처리할지를 결정합니다.
4. Controller:
• 컨트롤러는 @RequestMapping 또는 @GetMapping, @PostMapping과 같은 애노테이션을 통해 특정 URL 요청을 처리하는 메서드를 정의합니다. 요청을 처리한 후, 모델 데이터를 뷰에 전달하는 역할을 합니다.
5. Model:
• 컨트롤러에서 처리한 비즈니스 로직의 결과로 데이터를 모델에 담아서 뷰로 전달합니다. 이 데이터는 주로 ModelAndView 객체에 저장되며, model.addAttribute()와 같은 방식으로 전달할 수 있습니다.
6. View Resolver:
• View Resolver는 컨트롤러가 반환한 뷰 이름을 실제 뷰 파일로 변환합니다. 예를 들어, JSP 파일이나 Thymeleaf 템플릿을 렌더링할 수 있습니다. 이를 통해 View가 실제로 렌더링되어 사용자에게 표시됩니다.
7. Response:
• 최종적으로 생성된 뷰는 클라이언트에게 HTTP 응답으로 반환되며, 사용자는 결과를 웹 페이지로 확인할 수 있습니다.
RestController의 동작
1. 클라이언트 요청:
• 사용자가 HTTP 요청을 보냅니다. (예: /api/users)
2. DispatcherServlet:
• DispatcherServlet이 요청을 받으면, 요청 URL에 맞는 HandlerMapping을 통해 적합한 @RestController를 찾습니다.
3. RestController (Controller):
• @RestController에 정의된 메서드는 HTTP 응답 본문에 데이터(주로 JSON 형식)를 반환합니다. @ResponseBody가 자동으로 적용되므로, 반환된 객체는 뷰 템플릿을 거치지 않고 직접 HTTP 응답 본문으로 변환됩니다.
4. Model:
• 모델 데이터를 JSON 형태로 변환하여 클라이언트에 응답합니다. 보통 Jackson 또는 Gson 라이브러리가 이 역할을 맡습니다.
5. Response:
• 최종적으로 JSON 형식으로 변환된 데이터가 클라이언트에게 응답으로 전달됩니다.
Spring Boot는 어떤 편리한 점이 있을까요?
필요에 맞는 설정을 자동으로 제공합니다. 내장서버가 있어 별도의 서버 설치가 필요 없습니다. 또한 스타터 의존성이 제공되어 필요한 라이브러리들을 간편하게 설정할 수 있습니다.
다양한 프로파일을 지원하여 개발 환경에 맞는 프로파일 설정이 가능합니다.
Spring Boot Actuator가 애플리케이션 성능, 상태, 건강 상태등을 파악할 수 있습니다.
예를 들어 @SpringBootApplication 어노테이션은 자동설정, 컴포넌트 스캔, 설정 클래스를 자동으로 설정합니다.
Spring의 3가지 특징은 무엇인지 알고 계신가요?
Spring Framework는 Java 기반의 오픈소스 애플리케이션 프레임워크로, 기업용 애플리케이션을 개발하는 데 필요한 여러 기능들을 제공합니다. Spring은 크게 IoC(제어의 역전), AOP(관점 지향 프로그래밍), 트랜잭션 관리 등을 중심으로 한 모듈화가 특징입니다. Spring의 3가지 주요 특징은 의존성 주입(DI/IoC), AOP(Aspect-Oriented Programming), PSA입니다.
PSA (Portable Service Abstraction)는 Spring Framework에서 제공하는 서비스 추상화 개념으로, 특정 서비스나 라이브러리의 구현에 의존하지 않고 다양한 환경에 독립적인 방식으로 서비스에 접근할 수 있도록 하는 기법입니다. 즉, 서비스의 구현을 추상화하여, 실제 서비스 구현체를 쉽게 변경할 수 있도록 도와주는 방법론입니다.
PSA는 주로 클라우드 플랫폼, 데이터베이스 연결, 메시징 시스템 등과 같은 외부 서비스를 사용할 때 유용합니다. 개발자가 서비스 구현체를 변경해야 할 때, 서비스 호출 방법은 변경하지 않고도 쉽게 다른 구현체로 바꿀 수 있게 합니다.
-> DI의 주입 방식에 대해서 설명해주세요
- 생성자 주입
객체 생성 시점에 주입되므로 불변성을 유지할 수 있는 장점이 있습니다. - setter 주입
선택적 의존성에 사용되며, 객체 생성 후에도 의존 객체를 주입할 수 있습니다.
setter로 주입하지 않으면 null 발생 위험
불변성 유지 못함 - 필드 주입
직접 @Autowired 어노테이션을 붙여서 의존 객체를 주입받습니다. 테스트와 유지 보수면에서 단점이 있습니다.
final 키워드를 붙일 수 없기 때문에 불변성을 보장하지 못하고 Mock객체 주입을 할 수 없기 때문에 다른 객체로 변경될 위험이 있다.
순환 의존성이 런타임에서 발견될 수 있어 위험하다.
주입 방식 특징 장점 단점
생성자 주입 객체 생성 시 필수 의존성 주입 - 의존성의 불변성 보장 - 생성자 파라미터가 많아질 수 있음
세터 주입 객체 생성 후 의존성 주입 - 선택적 의존성 주입 가능 - 객체 불완전 상태를 만들 수 있음
필드 주입 필드에 직접 의존성 주입 - 간단하고 코드가 간결 - 테스트와 유지보수에서 불편함
스프링은 Bean을 어떻게 관리하는지 설명해주세요
Spring에서는 IoC (Inversion of Control) 컨테이너를 사용하여 애플리케이션 객체를 관리합니다. 이 IoC 컨테이너는 스프링 애플리케이션의 핵심으로, 애플리케이션에서 필요한 객체들을 생성하고, 설정하고, 의존성 주입(DI)을 처리합니다. 이때 각 객체를 Bean이라고 하며, 스프링은 이 Bean들을 관리하는 역할을 합니다.
생명주기
- Bean 생성
스프링은 애플리케이션 실행 시 IoC 컨테이너를 통해 Bean을 생성합니다. 이때, 애플리케이션에서 필요로 하는 객체를 미리 생성하여 관리합니다. - 초기화
Bean이 생성된 후 초기화 작업이 필요하면, @PostConstruct 애노테이션이나 InitializingBean 인터페이스를 구현하여 초기화 작업을 할 수 있습니다.
설정된 초기화 메서드가 실행됩니다. - 사용
Bean은 스프링 IoC 컨테이너에서 관리되며, 애플리케이션의 다른 부분에서 의존성 주입(DI)을 통해 Bean을 사용할 수 있습니다. - 소멸
애플리케이션 종료 시, @PreDestroy 애노테이션이나 DisposableBean 인터페이스를 사용하여 소멸 작업을 할 수 있습니다. 이때, 자원 반납 및 정리 작업을 진행합니다.
자동 소멸: 스프링은 싱글톤 Bean의 경우, 애플리케이션 종료 시 컨테이너가 종료될 때 자동으로 Bean을 파괴합니다. 이때, 별도의 소멸 메서드를 정의하지 않았다면 스프링은 Garbage Collection(GC)을 통해 객체를 소멸시킵니다.
• 만약 자원 반납 등의 작업이 필요하다면 명시적으로 @PreDestroy 애노테이션을 사용하거나, DisposableBean 인터페이스를 구현하여 정의해야 합니다.
스프링은 Bean의 범위(Scope)를 설정할 수 있습니다. 기본적으로 스프링은 싱글톤(Singleton) 스코프를 사용하지만, 다양한 스코프를 제공하여 Bean의 생명주기를 더욱 유연하게 관리할 수 있습니다.
• Singleton: 기본값. 애플리케이션 전체에서 단 하나의 인스턴스만 존재하며, 모든 요청에 동일한 Bean을 반환합니다.
• Prototype: 요청 시마다 새로운 Bean 인스턴스가 생성됩니다.
• Request: HTTP 요청 당 하나의 Bean 인스턴스가 생성됩니다.
• Session: HTTP 세션 당 하나의 Bean 인스턴스가 생성됩니다.
• Application: ServletContext 당 하나의 Bean 인스턴스가 생성됩니다
- 필터와 인터셉터, AOP이 수행되는 시점이 어떻게 다를까요?
- 필터
클라이언트 요청이 서블릿에 도달하기 전에 수행됩니다. 주로 로그 기록, 보안체크, 인코딩 처리에 사용됩니다. - 인터셉터
DispatcherServlet이 요청을 핸들러 메서드로 전달하기 전에 실행되며 핸들러 메서드 실행 후 응답이 뷰로 렌더링되기 전에도 수행됩니다. - AOP
핸들러 메서드 실행 전,후에 적용할 수 있습니다. 주로 메서드 호출 시점에 동작합니다. 트랜잭션 관리, 메서드 전/후 로깅, 성능 모니터링에 사용됩니다.
그럼 GlobalExceptionHandler는 어디에 속할까요?
GlobalExceptionHandler는 @ControllerAdvice 어노테이션을 사용하여 정의
되며 특정 예외를 처리하는 메서드를 작성하고 전체 컨트롤러에 적용할 수 있습니다. 이는 인터셉터와 유사하지만 정확히 일치하지는 않습니다. 인터셉터는 전후처리가 가능하지만 핸들러는 전체 흐름을 처리하기 때문입니다.
Sprig Security 작동방식에 대해 설명해보거나 아키텍처를 그려보세요
1. 클라이언트 요청: 클라이언트가 요청을 보내면, Spring Security의 필터 체인이 요청을 가로챕니다.
2. 인증 과정: 요청이 로그인 요청이면 UsernamePasswordAuthenticationFilter와 같은 필터를 통해 인증이 수행됩니다. 인증에 성공하면, 사용자 정보는 SecurityContext에 저장됩니다.
3. 권한 부여: 인증된 사용자 정보는 FilterSecurityInterceptor에서 사용되어 리소스 접근에 대한 권한을 검사합니다.
4. 보안 예외 처리: 인증 실패, 권한 부여 실패 등의 예외는 ExceptionTranslationFilter가 처리합니다.
5. 최종 처리: 요청이 정상적으로 처리되면 응답이 반환됩니다.
Security 동작 과정
- Http Request 수신
- 요청이 들어오면, 인증과 권한을 위한 필터들을 통하게 된다.
- 유저가 인증을 요청할 때는 인증 메커니즘과 모델을 기반으로 한 필터들을 통과한다.
- HTTP 기본 인증 : BasicAuthenticationFilter
- 로그인 폼에 의해 요청된 인증 : UserPasswordAuthenticationFilter 통과
- AuthenticationFilter가 해당 요청을 인터셉터한다.
- 유저 자격을 기반으로 인증토큰(AuthenticationToken) 만들기
- username과 password를 요청으로 추출하여 유저 자격을 기반으로 인증 객체를 생성
- 대부분 인증메커니즘은 username과 password를 기반으로 한다
- username과 password는 UsernamePasswordAuthenticationToken을 만드는데 사용된다.
- Filter를 통해 AuthenticationToken 을 AuthenticationManager에 위임
- UsernamePasswordAuthenticationToken오브젝트가 생성된 후, AuthenticationManager의 인증 메서드를 호출
- AuthenticationManager는 인터페이스로 정의됨
- 실제 구현은 ProviderManager에서 한다.
- 한마디로 AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken객체를 전달
- 실제 프로그래밍 구현에서는 AuthenticationManagerBuilder를 이용해 아래에 정의된 UserServiceDetails를 매핑시켜준다.
- 해당 UserServiceDetails를 구현한 Service객체가 그 역할을 한다.
- ProviderManager는 AuthenticationProvider를 구성하는 목록을 갖는다.
- ProviderManager는 password Authentication 객체(UsernamePasswordAuthenticaionToken)를 기반으로 만들어진 인증을 시도한다.
- AuthenticationProvider의 목록으로 인증을 시도한다.
- 인증을 위해 제공하는 객체들(Provider)
- CasAuthenticationProvider
- JaasAuthenticationProvider
- DaoAuthenticationProvider
- OpenIDAuthenticationProvider
- RememberMeAuthenticationProvider
- LdapAuthenticationProvider
- UserDetailsService의 요구
- UserDetailsService는 username기반의 user details를 검색
- UserDetailsService는 인터페이스로, 인증하고자 하는 비즈니스로직을 정의한 Service레이어에서 구현을 실행하는 방식 이용
- AuthenticationProvider에서 제공하고 있는 DaoAuthenticationProvider를 사용
- UserDetails를 이용해서 User객체에 대한 정보를 검색
- UerDetails는 인터페이스로, 우리가 DB를 생성하기 위한 객체에서 정보를 가져올 때 사용한다.
- UserDetails가 User객체의 정보들을 UserDetailService에 전달
- DB에서 user객체에 매핑된 정보를 가져와 UserDetailService에 전달
- 전달된 User객체의 정보와 사용자가 요청한 인증 정보를 확인하는 로직을 해당 Service에 구현
@Transactional public Optional login(String adminId, String password) throws UsernameNotFoundException { //인증 로직 구현(아이디, 비밀번호를 확인하는 로직을 거친다.)}
- 인증 객체 또는 AuthenticationException
- 유저의 인증이 성공하면, 전체 인증정보를 리턴한다
- 인증에 실패하면 예외처리
- 인증에 성공하면 객체의 정보 전달
- 인증 종료
- Authenticaionmanager는 완전한 인증(Fully Populated Authentication)객체를 Authentication Filter에 반환
- SecurityContext에 인증 객체를 설정
- AuthenticationFilter는 인증 객체를 Security Context에 저장
- SecurityContextHolder는 세션 영역에 있는 Security Context에 Authentication 객체를 저장
- 전통적인 세션-쿠키 기반의 인증 방식을 사용한다는 것을 의미
- 인증/인가에 대해서 어떻게 구현하셨나요?
AuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 실행되도록 설정합니다. AuthenticationFilter는 JWT 토큰을 받아서 이를 검증하고, 유효한 경우 해당 사용자 정보를 SecurityContext에 저장하여 인증을 처리합니다.
• jwtHelper: JWT를 처리하는 유틸리티 객체로, 이 객체를 사용하여 JWT 토큰을 디코딩하고 검증할 수 있습니다.
• JWT가 유효한 경우 UsernamePasswordAuthenticationToken 객체를 만들어 인증 정보를 설정하고, 이를 SecurityContext에 저장하여 후속 요청에서 인증된 사용자를 확인할 수 있습니다. - 인가(Authorization)는 인증된 사용자가 특정 리소스에 접근할 권한이 있는지를 확인하는 과정입니다. 이 코드에서는 URL 패턴과 권한을 기반으로 인가를 설정하고 있습니다.
- 스프링에서 예외처리를 어떻게 구현하셨나요?
이 코드에서는 전역 예외 처리(Global Exception Handling)를 구현하기 위해 @RestControllerAdvice와 @ExceptionHandler를 사용하여 다양한 예외들을 처리하고 있습니다.
RuntimeException을 상속받은 커스텀 상위 객체를 만들고 이를 각 도메인별로 상속받아 사용하고 있습니다.
@RestControllerAdvice의 역할은 무엇인가요?
1. 전역 예외 처리: @RestControllerAdvice는 @ControllerAdvice와 유사하지만, @ResponseBody가 포함되어 있기 때문에 자동으로 응답을 JSON 형식으로 반환합니다. 이를 통해 예외를 전역적으로 처리하고, @ExceptionHandler를 사용하여 특정 예외를 다룰 수 있습니다.
2. 모든 @RestController에 대한 예외 처리: @RestControllerAdvice는 @RestController에서 발생하는 예외를 처리하는 데 사용됩니다. @RestController는 @Controller와 @ResponseBody를 결합한 어노테이션으로, 반환값을 자동으로 JSON으로 변환하여 클라이언트에 응답합니다. @RestControllerAdvice는 모든 @RestController에서 발생하는 예외를 처리할 수 있는 중앙집중식 예외 처리기를 제공합니다.
3. 응답 포맷 일관성 유지: 예외 발생 시, 모든 예외를 일관된 형식의 응답 객체로 변환하여 클라이언트에 전달합니다. 이를 통해 프론트엔드와 백엔드 간의 예외 처리 구조를 표준화할 수 있습니다.
Restful한 API는 어떻게 만들어야 할까요?
- API 자원, 행동, 주소를 잘 표현해야 한다. 각 메서드의 행위를 고려해봐야한다.
Spring에서 개선 경험이 있다면 어떤 기술을 써서 어떻게 개선했는지 말씀해주세요.
자동단어 완성 기능을 MySQL 의 LIKE로 구현했으나 Full Text Scan을 하는 방식은 DB를 부하시켜 성능을 떨어뜨릴 수 있다는 단점이 있었습니다. 그래서 이를 SortedSet, Trie라는 기능을 사용해서 구현해보았고 이 두개의 수행방식에서 성능 비교를 하기 위해 Jmeter를 사용해 10000번의 요청을 100명의 사용자가 보내도록 테스트하였습니다. SortedSet은 Redis저장소를 한 번 거쳐야 하는 부분에서 트래픽차이가 조금 있었지만 성능의 차이는 크게 없었습니다. SortedSet은 score로 순위까지 매길 수 있기에 인기검색어 기능을 추가로 구현해볼 수 있어 채택하게 되었습니다. 이 과정에서 기존 방식보다 성능은 30%정도 개선되는 수치를 보았습니다.
JPA
JPA
JPA에서 Entity의 Life Cycle은 어떻게 이루어지나요?
영속성 컨텍스트는 EntityManager가 관리하며, 이 컨텍스트에서 엔티티의 상태 변화를 추적합니다.
- 엔티티가 persist()를 호출하여 영속화되면, 영속성 컨텍스트에 의해 관리되기 시작합니다. 이때 엔티티는 Managed 상태로 전환됩니다.
- 영속성 컨텍스트가 종료되거나, clear() 또는 detach()를 호출하면 엔티티는 Detached 상태로 변합니다.
- 엔티티가 remove()되면, 영속성 컨텍스트에서 제거되고 Removed 상태로 전환됩니다.
@Transactional에 readOnly=true속성을 사용하는 이유에 대해 설명해주세요.
@Transactional의 readOnly = true 속성은 주로 읽기 전용 트랜잭션을 설정하는 데 사용됩니다. 이 속성은 주로 데이터베이스 성능 최적화와 관련이 있으며, 트랜잭션이 읽기 작업만 수행할 것임을 명시합니다. 이 속성의 주요 목적과 사용 이유는 다음과 같습니다.
- 읽기 전용 트래잭션을 사용하여 더 적은 자원을 사용해 성능을 최적화 할 수 있습니다. 쓰기작업에 필요한 lock을 피할 수 있습니다.
- 트랜잭션 롤백 방지로 쓰기 작업이 발생하지 않음을 보장하려는 목적이 있습니다.
- 예외 처리 내부에서 수정 작업이 시도되면, 해당 트랜잭션에서 예외를 던져 종료시킬 수 있습니다.
JPA에서 Eager Loading과, Lazy Loading의 차이점은 무엇인가요?
즉시 로딩은 연관된 엔티티를 즉시 로딩시키는 방법으로 연관된 데이터를 쿼리와 함께 즉시 가져오도록 설정하는 방법입니다. @ToOne으로 끝나는 어노테이션은 기본적으로 EagerLoading을 사용합니다. 데이터르 ㄹ한 번에 가져오기때문에 성능상 효율적일수도 있으나 불필요한 연관관계 데이터를 불러오게 되면 성능 저하를 일으킬 수 있습니다. N+1 문제도 발생하지 않습니다.
지연 로딩은 연관된 엔티티가 필요할 때 로딩하는 방식입니다. 실제로 해당 데이터를 참조할 때 새로운 쿼리를 실행하여 데이터를 가져옵니다. 성능이 최적화 될 수는 있지만 연관된 데이터를 미리 가져오지 않기 때문에 N+1 문제가 발생할 수 있습니다.
JPA에서 FetchType.LAZY로 설정했을 때 N+1문제가 발생하는 이유는 무엇인가요?
N은 연관된 자식의 엔티티 수이고 +1은 부모 엔티티를 조회하는 쿼리입니다. 부모엔티티를 조회한 후에 자식 엔티티를 조회할 때에는 추가적으로 쿼리가 실행됩니다.
fetch join을 사용해도 발생할 수 있는 문제가 있지 않나요?
• fetch join을 사용하면 모든 데이터를 가져온 후 메모리에서 페이징을 수행함
• 즉, 쿼리에서 LIMIT, OFFSET이 적용되지 않음
• 페이징을 위해 필요한 데이터보다 더 많은 데이터를 불러오므로 성능 저하 발생
N+1문제를 해결하기 위한 방법과 각 방식의 장단점을 말해주세요.
이를 해결하기 위한 방법은 fetch join을 사용해서 한 번의 쿼리로 조회하는 방법입니다.
또는 EntityGraph를 사용하여 특정 연관관계를 미리 로딩할 수 있습니다.
또는 BatchSize를 설정하여 연관된 엔티티를 가질때 해당 사이즈만큼 묶어서 조회하게 됩니다. 예를 들어 100개의 product와 Product에 Review가 5개씩 있다고 가정할때, 100개의 Product에 대해서 정한 사이즈의 수만큼 묶어서 Review를 가져오도록 설정 가능합니다.
JPA의 변경감지 (Dirty Checking)는 어떻게 동작하나요?
핵심 동작
1. 영속성 컨텍스트 내에서 엔티티의 상태를 추적합니다.
2. 엔티티 객체의 상태가 변경되면, 트랜잭션 커밋 시 그 변경 사항을 감지하고, 변경된 필드만을 데이터베이스에 업데이트합니다.
3. 불필요한 업데이트를 하지 않기 때문에 성능 최적화가 가능합니다.
Dirty Checking의 특징
• 명시적 저장 없이 자동으로 동작: 엔티티 객체가 변경되면, 트랜잭션 커밋 시 JPA가 자동으로 감지하고 변경된 내용을 DB에 반영합니다. 개발자가 따로 update 메서드를 호출할 필요가 없습니다.
• 변경된 데이터만 업데이트: JPA는 엔티티의 상태를 추적하고, 트랜잭션 커밋 시 변경된 데이터만 SQL로 업데이트합니다. 불필요한 필드는 업데이트하지 않기 때문에 효율적입니다.
• 영속성 컨텍스트에 있는 엔티티만 감지: 영속성 상태(Persistent state)의 엔티티만 추적합니다. 비영속 상태(Transient state)나 준영속 상태(Detached state)인 엔티티는 감지되지 않습니다. 그러므로 객체가 영속성 컨텍스트에 있어야만 변경 사항을 감지할 수 있습니다.
OSIV (Open Session In View)가 무엇이며, true/false 설정에 대한 차이점을 알려주세요.
OSIV는 Open Session in View의 약자로, Spring Framework에서 제공하는 기능 중 하나입니다. OSIV는 웹 요청을 처리하는 동안 Hibernate 세션을 열어 두고, 뷰 렌더링(즉, JSP나 Thymeleaf와 같은 템플릿에서 데이터를 렌더링하는 과정) 중에 데이터를 지연 로딩(Lazy Loading)할 수 있도록 하는 방법입니다.
- spring.jpa.open-in-view=true (OSIV 활성화)
• 장점:
• 지연 로딩(Lazy Loading)을 사용할 때, 엔티티의 관계를 뷰 렌더링 시점까지 지연시킬 수 있습니다.
• 세션이 요청의 전체 범위에서 열려 있으므로, 요청 처리 후에도 추가적인 데이터 조회가 가능합니다. 예를 들어, 엔티티에서 연관된 데이터를 지연 로딩 방식으로 조회할 수 있습니다.
• 단점:
• 뷰 렌더링 시에 데이터베이스 쿼리가 추가로 발생할 수 있습니다. 이로 인해 느린 성능이 발생할 수 있으며, 특히 연관된 엔티티가 많은 경우 N+1 문제를 유발할 수 있습니다.
• 세션을 요청 처리 이후까지 열어두기 때문에, 트랜잭션 범위 외에서 데이터베이스와 연결된 상태로 두는 것에 대한 리소스 낭비나 의도하지 않은 상태 변경이 발생할 수 있습니다. - spring.jpa.open-in-view=false (OSIV 비활성화)
• 장점:
• 세션을 뷰 렌더링과는 독립적으로 관리하므로, 세션이 종료된 후 더 이상 데이터베이스에 접근할 수 없게 됩니다.
• 트랜잭션 범위 내에서만 세션을 열어두기 때문에 DB 세션을 불필요하게 열어놓지 않아 성능 상 유리합니다.
• 지연 로딩을 사용할 경우 불필요한 쿼리 발생을 방지할 수 있습니다.
• 단점:
• 지연 로딩이 필요한 경우에는 LazyInitializationException이 발생할 수 있습니다. 이 예외는 세션이 이미 닫힌 후에 지연 로딩을 시도할 때 발생합니다.
• 뷰에서 데이터를 렌더링하기 전에 모든 관련 데이터를 미리 즉시 로딩(Eager Loading) 방식으로 로딩해야 합니다. 그렇지 않으면 뷰 렌더링 시점에 필요한 데이터를 조회할 수 없게 됩니다.
JPA의 1차캐시와2차캐시의 차이에 대해 설명해주세요.
- 1차 캐시
주요 특징:
• 영속성 컨텍스트(주로 EntityManager) 내부에서만 유효합니다.
• EntityManager가 관리하는 세션 내에서만 엔티티가 캐시됩니다.
• 트랜잭션 단위로 관리되며, 트랜잭션이 끝나면 캐시도 초기화됩니다.
• 기본적으로 활성화 되어 있으며, 이를 비활성화할 수 없습니다.
• 동일한 EntityManager 내에서 동일한 엔티티에 대한 조회가 이루어지면 데이터베이스 쿼리가 발생하지 않고, 캐시된 값을 반환합니다. - 2차 캐시
2차 캐시는 애플리케이션 범위에서 사용되는 캐시로, 여러 EntityManager 인스턴스 간에 공유됩니다. 2차 캐시는 JPA 공급자에 따라 별도로 설정해야 하며, 디스크나 메모리에 저장됩니다.
주요 특징:
• EntityManager를 넘어서 애플리케이션 범위에서 캐시가 공유됩니다. 즉, 애플리케이션 내 모든 EntityManager 인스턴스에서 접근 가능합니다.
• 세션을 넘어서 데이터를 저장하므로 애플리케이션 전체에서 데이터를 재사용할 수 있습니다.
• JPA 구현체에 따라 설정이 필요합니다. 예를 들어, Hibernate에서는 2차 캐시를 활성화하기 위해 추가적인 설정이 필요합니다.
• 데이터 일관성을 보장하려면 주기적으로 캐시를 갱신하거나 만료 정책을 설정해야 합니다.
• 1차 캐시와 달리 별도로 설정하지 않으면 비활성화 상태입니다.
- JPA에서 @Transactional의 Propagation 은 무엇인가요 ?
@Transactional의 Propagation 종류- REQUIRED (기본값)
• 설명: 호출된 메소드가 이미 트랜잭션 내에서 실행 중이면 그 트랜잭션에 참여합니다. 만약 트랜잭션이 없으면 새 트랜잭션을 시작합니다.
• 사용 예: 일반적으로 사용되는 기본 설정입니다. 외부 트랜잭션이 없으면 새로 시작하고, 있으면 기존 트랜잭션에 참여합니다. - REQUIRES_NEW
• 설명: 항상 새로운 트랜잭션을 시작합니다. 기존에 실행 중인 트랜잭션이 있다면 이를 잠시 보류하고, 새로운 트랜잭션을 시작합니다. 작업이 완료되면 보류 중이던 트랜잭션이 다시 실행됩니다.
• 사용 예: 하나의 트랜잭션이 실패하더라도 그 트랜잭션에 영향을 미치지 않도록 독립적인 트랜잭션을 사용하고 싶을 때 사용됩니다. - SUPPORTS
• 설명: 이미 트랜잭션이 존재하면 해당 트랜잭션에 참여하고, 존재하지 않으면 트랜잭션 없이 실행됩니다.
• 사용 예: 트랜잭션이 없어도 동작해야 하는 작업에서 사용됩니다. - NOT_SUPPORTED
• 설명: 트랜잭션이 존재하면 이를 중단하고 트랜잭션 없이 실행합니다. 만약 트랜잭션이 없다면 트랜잭션 없이 실행됩니다.
• 사용 예: 트랜잭션이 필요하지 않거나, 트랜잭션을 잠시 중단해야 하는 경우에 사용됩니다. - MANDATORY
• 설명: 반드시 트랜잭션이 존재해야 합니다. 만약 트랜잭션이 없으면 예외가 발생합니다.
• 사용 예: 트랜잭션이 반드시 필요한 경우에 사용됩니다. - NEVER
• 설명: 트랜잭션이 있으면 예외를 발생시키고, 트랜잭션 없이 실행됩니다.
• 사용 예: 트랜잭션 없이만 실행되어야 하는 경우 사용됩니다.
• 설명: 현재 트랜잭션이 있으면 그 트랜잭션 내에서 중첩 트랜잭션을 시작합니다. 중첩 트랜잭션이 커밋되거나 롤백되면 전체 트랜잭션이 커밋되거나 롤백됩니다. SAVEPOINT와 ROLLBACK TO SAVEPOINT 기능을 지원하는 DB에서만 동작합니다.
• 사용 예: 내부적으로 작은 트랜잭션을 분리하여 관리하고 싶을 때 사용됩니다.
7. NESTED
- REQUIRED (기본값)
JPA에서 Proxy란 무엇이고 언제, 어떻게동작하나요 ?
JPA가 엔티티를 로딩할 때, 실제 엔티티 객체를 바로 로딩하지 않고, 해당 엔티티의 프록시 객체를 생성합니다. 이 프록시 객체는 실제 엔티티를 대신할 수 있으며, 내부에 클래스의 메타데이터나 식별자 정보를 저장합니다.
1. 프록시 객체 생성
• JPA가 엔티티를 로딩할 때, 실제 엔티티 객체를 바로 로딩하지 않고, 해당 엔티티의 프록시 객체를 생성합니다. 이 프록시 객체는 실제 엔티티를 대신할 수 있으며, 내부에 클래스의 메타데이터나 식별자 정보를 저장합니다.
2. 실제 엔티티 로딩
• 프록시 객체의 메서드가 호출되면, 이 메서드는 실제 데이터베이스로부터 해당 엔티티를 로딩하는 추가 쿼리를 실행하게 됩니다. 이렇게 해서 실제 엔티티가 로딩되고, 그 이후에는 로딩된 엔티티를 프록시가 대신하여 반환합니다.
3. 프록시 객체의 특징
• 프록시 객체는 **javax.persistence.HibernateProxy**와 같은 Hibernate가 제공하는 프록시 클래스로 구현됩니다.
• 프록시 객체는 Lazy InitializationException을 방지하기 위해, 실제 데이터를 사용할 때까지 쿼리를 보내지 않고 데이터베이스에서 조회하지 않습니다.
언제 Proxy가 동작하나요?
1. Lazy Loading을 사용하는 경우
• JPA에서 엔티티 간 관계를 Lazy Loading으로 설정하면, 관련된 엔티티는 실제로 사용할 때까지 프록시 객체를 통해 대체됩니다.
• fetch = FetchType.LAZY로 설정된 관계에서 Proxy가 동작하며, 실제 엔티티를 지연 로딩합니다.
2. 프록시 객체의 초기화
• 프록시 객체는 실제 데이터가 필요할 때 로딩됩니다. 예를 들어, Order 객체에서 User 객체를 getUser() 메서드로 호출하는 순간, 프록시 객체가 실제 User 엔티티로 초기화됩니다.
3. 영속성 컨텍스트 내에서만 동작
• 프록시 객체는 영속성 컨텍스트(Persistence Context) 내에서만 동작합니다. 즉, 트랜잭션이 종료되거나 영속성 컨텍스트가 종료되면 프록시 객체는 더 이상 사용할 수 없으며, 해당 객체에 대한 추가적인 작업을 할 수 없습니다.
Redis
- Redis란 무엇이며, 어떤 특징을 가지고 있나요?
- 인메모리 저장소
Redis는 데이터를 디스크가 아닌 메모리(RAM)에 저장합니다. 이로 인해, 디스크 기반 저장소보다 훨씬 빠른 읽기/쓰기 속도를 자랑합니다. 따라서 실시간 데이터 처리나 빠른 응답이 필요한 애플리케이션에서 유용하게 사용됩니다. - 다양한 데이터 구조 지원
Redis는 단순한 key-value 저장소를 넘어서 여러 가지 복잡한 데이터 구조를 지원합니다.
Redis를 사용하여 마이크로서비스 간 데이터를 공유하는 방법은 무엇인가요?
- 공유 캐시를 사용하여 여러 마이크로 서비스가 동일한 데이터를 빠르게 조회할 수 있습니다.
- 세션 공유를 통해 세션 스토리지에서 여러 스비스가 동일한 사용자 세션에 접근할 수 있도록 할 수 있습니다.
- 분산 잠금 기능을 통해 여러 서비스가 공유 리소스를 안전하게 사용할 수 있습니다. 여러 마이크로서비스가 하나의 리소스를 동시에 수정하는 경우, 분산 잠금을 사용하여 경쟁 조건을 방지할 수 있습니다.
- List나 Queue 방식을 사용하여 마이크로서비스 간에 데이터를 주고 받을 수 있습니다. 하나의 서비스에서 큐에 데이터가 들어가면 서비스가 이를 처리하는 비동기 처리에 적합합니다.
- 데이터 동기화 및 이벤트 소싱으로 Redis에 이벤트를 기록하고, 다른 서비스는 해당 이벤트를 구독하여 상태를 동기화 할 수 있습니다.
MSA 환경에서 Redis를 활용한 분산 락(Distributed Lock)은 어떻게 구현하나요?
Redis를 이용한 분산 락 구현은 여러 가지 방법이 있지만, 대표적으로 SETNX 명령어와 Redisson 라이브러리 같은 도구를 사용합니다.
SETNX 명령어는 key가 존재하지 않으면 값을 설정하고, key가 이미 존재하면 아무 작업도 하지 않는다는 특징을 가지고 있습니다. 이를 이용해 락을 구현할 수 있습니다.
기본 흐름
1. 락 획득: 서비스가 리소스를 수정하려고 할 때, Redis에 특정 key(예: lock:resource)를 설정합니다. 이때 SETNX를 사용하여 이미 락이 걸려 있지 않다면 설정하고, 락을 획득합니다.
2. 리소스 작업: 락을 획득한 서비스는 리소스를 안전하게 수정할 수 있습니다.
3. 락 해제: 리소스 작업이 끝나면 Redis에서 락을 제거합니다.
Redisson은 Redis를 이용한 분산 시스템을 위한 고급 클라이언트 라이브러리로, 분산 락을 쉽게 사용할 수 있도록 API를 제공합니다. RedissonLock을 사용하여 락을 획득하고 해제할 수 있습니다.
기본 흐름
1. 락 획득: RedissonLock을 사용하여 Redis에서 제공하는 락을 획득합니다.
2. 리소스 작업: 락을 획득한 후 안전하게 리소스를 수정합니다.
3. 락 해제: 작업이 끝난 후 락을 해제합니다.
Redis를 이용한 분산 락 사용 시 고려할 점
• 락 만료 시간 설정: Redis에서 락을 설정할 때, 작업이 완료되지 않더라도 락이 만료되도록 타임아웃을 설정해야 합니다. 만약 락이 만료되지 않으면 다른 서비스가 락을 얻지 못하고 대기하게 될 수 있습니다.
• 락의 정확한 해제: 락을 사용하는 서비스가 예기치 않게 종료되면 락이 풀리지 않을 수 있습니다. 이를 방지하기 위해 락을 반드시 작업이 완료된 후에 해제해야 하며, 타임아웃을 활용하여 일정 시간이 지나면 자동으로 락을 해제할 수 있도록 처리해야 합니다.
• 락의 경쟁 상태: 여러 서비스가 동시에 락을 획득하려고 하면, 락 경합이 발생할 수 있습니다. 이 경우, 락을 획득할 때 대기하는 시간이 길어지거나 성능에 영향을 줄 수 있으므로 적절한 분배 전략을 고려해야 합니다.
Redis는 왜 NoSQL 데이터베이스로 분류되나요?
데이터 모델이 키-값 저장소로 분류되며 스키마가 없어 다양한 타입으로 저장하고 수정할 수 있습니다.
Redis를 활용하여 세션 저장소로 사용할 경우의 장점과 단점은 무엇인가요?
- 장점
- 고속 처리
- 자동 만료
- 확장성
- 다양한 데이터 구조 지원
- 신뢰성 및 내결함성
- 다중 플랫폼 지원
- 단점
- 메모리 제한
- 데이터 영속성 문제
- 세션 만료와 동기화 문제
Redis를 캐시(Cache)로 사용할 때의 장점과 단점을 설명해주세요.
장점
- 고속 성능
• Redis는 인메모리 데이터베이스로, 디스크 기반 저장소에 비해 읽기/쓰기 속도가 매우 빠릅니다. 이는 애플리케이션에서 자주 조회되는 데이터를 캐시할 때 성능을 크게 향상시킬 수 있습니다. 특히, 데이터베이스나 외부 API 호출에서 발생하는 지연을 줄여주어 애플리케이션의 응답 속도를 개선할 수 있습니다.- 효율적인 데이터 조회
• 캐시로 Redis를 사용하면 데이터베이스에 대한 접근을 줄일 수 있어 데이터베이스 부하를 감소시키고, 애플리케이션이 빠르게 응답할 수 있게 도와줍니다. 자주 사용되는 데이터를 Redis에 캐시해두면, 동일한 데이터에 대한 반복적인 요청을 데이터베이스 대신 Redis에서 처리할 수 있습니다. - 자동 만료 및 TTL (Time to Live)
• Redis는 각 캐시 항목에 만료 시간(TTL)을 설정할 수 있는 기능을 제공합니다. 이를 통해 일정 시간이 지난 후 캐시된 데이터를 자동으로 제거하여 메모리 관리를 할 수 있습니다. 예를 들어, 자주 변하지 않는 데이터를 일정 시간이 지나면 삭제하고, 새로운 데이터로 갱신할 수 있습니다. - 유연한 데이터 구조 지원
• Redis는 단순한 키-값 저장소 뿐만 아니라 리스트, 집합, 정렬된 집합, 해시 등 다양한 데이터 구조를 지원합니다. 이러한 다양한 데이터 구조를 활용하여 캐시 데이터를 더욱 효율적으로 저장하고 관리할 수 있습니다. 예를 들어, 정렬된 집합을 사용하여 페이지네이션된 데이터를 캐시하거나, 해시를 사용하여 관련 데이터를 그룹화할 수 있습니다. - 분산 캐시
• Redis는 분산 환경에서의 확장성을 지원합니다. 여러 Redis 인스턴스를 클러스터링하여 데이터를 분산시킬 수 있으며, 이를 통해 높은 확장성과 고가용성을 제공할 수 있습니다. Redis 클러스터를 사용하면 서버 간에 데이터를 분산시켜 더 많은 요청을 처리할 수 있습니다. - 데이터 영속성 옵션
• Redis는 기본적으로 인메모리 방식이지만, AOF(Append-Only File) 또는 RDB(Snapshot) 방식으로 데이터를 영속적으로 저장할 수 있습니다. 이를 통해 캐시 데이터를 디스크에 저장하여 재시작 후에도 데이터를 복원할 수 있습니다. 영속성을 원하지 않는 경우에는 비휘발성 메모리 캐시로만 활용할 수 있습니다.
- 효율적인 데이터 조회
단점
• Redis는 메모리 기반 데이터베이스로, 데이터가 메모리에 저장되기 때문에 메모리 용량에 의존합니다. 서버의 메모리 용량을 초과한 데이터를 저장하면 Out of Memory(OOM) 오류가 발생할 수 있습니다. 따라서 캐시 데이터의 크기나 수가 많아지면 메모리 관리가 중요한 문제가 될 수 있습니다.
2. 데이터 영속성 문제
• Redis는 기본적으로 인메모리 캐시로 데이터를 저장하므로, 서버가 재시작되면 데이터가 사라질 수 있습니다. 영속성을 보장하려면 AOF 또는 RDB 방식으로 데이터를 디스크에 저장해야 하지만, 이로 인해 성능 저하가 있을 수 있습니다. 영속성을 원하지 않거나 불필요한 경우, 서버 재시작 시 캐시가 사라지는 문제가 발생할 수 있습니다.
3. 일관성 문제
• 캐시된 데이터는 데이터베이스와 동기화되지 않는 경우가 많습니다. 즉, 데이터베이스에 변경이 있을 때 캐시된 데이터를 수동으로 갱신해야 하는데, 이를 자동화하지 않으면 캐시 불일치(Cache Inconsistency) 문제가 발생할 수 있습니다. 예를 들어, 데이터베이스에서 업데이트된 내용이 캐시에 반영되지 않으면 오래된 데이터가 계속 반환될 수 있습니다.
4. 데이터 관리 복잡성
• Redis에서 캐시를 사용할 때, 캐시 무효화 전략(cache invalidation) 관리가 중요한 이슈가 될 수 있습니다. 예를 들어, 특정 데이터가 변경될 때 해당 데이터를 캐시에서 어떻게 처리할지 결정해야 합니다. **LRU(Least Recently Used)**나 TTL(Time-To-Live) 방식으로 만료시간을 설정하거나, 캐시 업데이트/삭제를 어떻게 처리할지 관리해야 하는 등의 복잡한 작업이 필요할 수 있습니다.
5. 네트워크 부하
• Redis는 네트워크를 통해서 클라이언트와 상호작용하기 때문에 네트워크 지연이나 부하가 발생할 수 있습니다. 분산 캐시 시스템에서 Redis를 사용하는 경우, 네트워크 병목현상이 캐시 성능에 영향을 미칠 수 있습니다. 특히 많은 클라이언트가 동시에 캐시를 요청할 때 Redis 서버에 과부하가 걸릴 수 있습니다.
6. 데이터 크기에 따른 성능 저하
• 캐시 데이터가 지나치게 커지면, Redis는 데이터를 처리하는 데 필요한 메모리와 CPU 자원을 더 많이 소모할 수 있습니다. 특히 큰 데이터를 캐시할 경우, Redis 서버의 성능이 저하될 수 있으며, 결국 캐시의 효율성이 떨어질 수 있습니다.
Redis의 데이터 구조와 기본적인 자료형을 설명해주세요.
- String
- List
- Set
- SortedSet(score와 함께 저장)
- Hash
- Bitmaps
- HyperLogLog(대규모의 유니크한 항목 수를 세는데 사용)
- Stream(실시간 데이터 스트리밍 및 이벤트 처리)
Redis는 싱글 스레드 기반인데도 성능이 뛰어난 이유는 무엇인가요?
- Redis는 기본적으로 매우 간단한 데이터 구조를 사용하고, 연산도 매우 효율적입니다. 예를 들어, 문자열, 리스트, 셋, 정렬된 셋 등은 모두 특정 패턴의 연산만 필요로 합니다. 복잡한 조인, 트랜잭션, 또는 복잡한 쿼리를 다루는 전통적인 RDBMS와 달리 Redis는 단일 연산에 집중합니다. 따라서, 연산 속도가 매우 빠릅니다.
- Redis는 인메모리 데이터베이스로, 데이터를 디스크가 아닌 메모리에서 직접 읽고 씁니다. 메모리는 디스크보다 훨씬 빠르기 때문에 디스크 I/O 대기 시간을 제거할 수 있습니다. 또한, 메모리 내에서 데이터를 관리하는 방식이 매우 효율적입니다. 예를 들어, Redis는 메모리 최적화와 가비지 컬렉션을 잘 관리하여 데이터를 빠르게 처리합니다.
- Redis가 싱글 스레드로 동작하는 이유는 멀티스레딩에서 발생할 수 있는 복잡한 동기화 문제를 피하기 위해서입니다. 여러 스레드를 사용하면 락(Lock)을 관리해야 하므로 병렬 처리에 필요한 동기화 비용이 발생할 수 있습니다. Redis는 싱글 스레드로 이러한 문제를 해결하고, 동시에 연산을 빠르게 처리할 수 있습니다. 하나의 스레드가 순차적으로 모든 작업을 처리하므로, 경쟁 조건(race condition)이나 컨텍스트 스위칭 비용이 없고, CPU 캐시도 효율적으로 사용할 수 있습니다.
그럼 병목현상이 아예 발생하지 않나요?
Redis도 병목 현상이 발생할 수 있어! Redis가 싱글 스레드 기반으로 동작한다고 해도, 모든 경우에서 병목이 완전히 사라지는 것은 아닙니다.
1️⃣ CPU 병목
✅ Redis는 싱글 스레드로 동작하지만, CPU 연산량이 많아지면 병목이 생길 수 있음.
🔹 예제)
• 복잡한 연산 (Lua 스크립트, 정렬, 대량의 데이터 처리)
• 클라이언트 요청이 너무 많아 큐에 쌓이는 경우
➡ 해결 방법
✔ 멀티 코어를 활용하기 위해 Redis 샤딩(Clustering) 사용
✔ CPU 연산이 많은 작업은 적절한 캐싱 전략을 활용하여 줄이기
2️⃣ 네트워크 병목
✅ Redis는 네트워크 I/O가 발생하는 서비스이기 때문에 네트워크 속도가 제한 요소가 될 수 있음.
🔹 예제)
• 한 번에 너무 많은 데이터(대량의 키-값, 큰 리스트)를 전송하는 경우
• 멀티 노드 환경에서 네트워크 트래픽이 과부하되는 경우
➡ 해결 방법
✔ Pipelining을 활용하여 여러 개의 명령어를 한 번에 처리
✔ 대량 데이터 전송 시, 압축(Compression) 기능 활용
3️⃣ 메모리 병목
✅ Redis는 인메모리 데이터베이스이기 때문에 메모리가 부족해지면 성능이 저하될 수 있음.
🔹 예제)
• 사용하는 데이터 크기가 너무 커서 Redis 메모리를 초과하는 경우
• Eviction 정책이 비효율적으로 설정되어 중요한 데이터가 삭제되는 경우
➡ 해결 방법
✔ LRU(Least Recently Used) 또는 LFU(Least Frequently Used) 정책 활용
✔ 필요 없는 데이터 정리(GC) 및 TTL 설정하여 오래된 데이터 자동 삭제
4️⃣ 디스크 I/O 병목 (AOF, RDB 저장 시)
✅ Redis는 기본적으로 인메모리에서 작동하지만, 데이터를 디스크에 저장할 때 성능 저하가 발생할 수 있음.
🔹 예제)
• AOF(Append Only File) 모드에서 너무 자주 디스크에 쓰는 경우
• RDB 스냅샷 저장 시 디스크 I/O가 과부하되는 경우
➡ 해결 방법
✔ AOF 설정을 적절히 조정 (appendfsync everysec vs no-appendfsync-on-rewrite)
✔ RDB 스냅샷 저장 주기를 조정하여 디스크 I/O 부담 줄이기
📌 결론
Redis가 기본적으로 싱글 스레드 모델을 사용하여 락 문제를 방지하고 고속 연산을 지원하지만,
CPU, 네트워크, 메모리, 디스크 I/O 등에서 병목이 발생할 수 있음!
➡ 대량의 데이터 처리나 트래픽이 많은 환경에서는 클러스터링(Sharding)과 캐싱 전략을 잘 활용해야 함.
➡ Redis 설정을 적절히 조정하여 병목을 최소화하는 것이 중요함!
Redis의 영속성 방식에는 어떤 것들이 있으며, 각각의 차이점은 무엇인가요?
- RDB (Redis Database)
RDB는 스냅샷(Snapshot) 방식을 통해 Redis 데이터를 주기적으로 디스크에 저장하는 방식입니다. 주기적으로 데이터베이스 상태를 스냅샷으로 저장하고, 이를 파일로 저장합니다.
- 장점:
• 빠른 읽기 성능: 데이터가 메모리에만 존재하고, RDB는 스냅샷 파일을 통해 데이터를 복구하므로 읽기 성능이 뛰어납니다.
• 저장소 절약: 디스크에 저장되는 파일은 주기적인 스냅샷 파일이라 상대적으로 용량이 적습니다.
• 복구가 빠름: 전체 스냅샷을 저장하므로 서버 재시작 후 빠르게 데이터를 복구할 수 있습니다. - 단점:
• 데이터 유실 가능성: 마지막 스냅샷 이후의 데이터는 저장되지 않기 때문에 서버가 종료되거나 장애가 발생하면 마지막 스냅샷 이후의 데이터는 손실될 수 있습니다.
• 백그라운드 작업 부하: 스냅샷을 생성하는 동안에는 디스크 I/O가 발생하여 시스템 자원에 부하를 줄 수 있습니다.
- AOF (Append Only File)
- 장점:
• 데이터 유실 최소화: AOF는 모든 쓰기 작업을 기록하므로 실시간에 가까운 데이터 복구가 가능합니다.
• 데이터 안정성: AOF 파일은 모든 명령을 기록하므로 데이터를 정확히 복구할 수 있습니다.
• 유연한 동기화 옵션: fsync 설정에 따라 데이터 동기화 주기를 조정할 수 있습니다. - 단점:
• 디스크 I/O 성능 저하: AOF는 모든 쓰기 명령을 로그에 기록하므로 디스크 I/O가 자주 발생하고, 이는 성능에 영향을 줄 수 있습니다.
• 파일 크기 문제: AOF 파일은 시간이 지남에 따라 매우 커질 수 있으며, 이로 인해 디스크 공간을 많이 차지할 수 있습니다.
• 복구 시간이 길어질 수 있음: AOF 로그를 순차적으로 재생하여 복구하기 때문에 데이터 양이 많을수록 복구 시간이 오래 걸릴 수 있습니다.
AOF는 모든 쓰기 명령을 로그 형태로 기록하는 방식입니다. Redis에서 수행한 모든 쓰기 작업(SET, DEL, INCR 등)을 단순 텍스트 로그 파일에 기록하며, 서버가 재시작할 때 이 파일을 사용하여 데이터베이스를 복구합니다.
- RDB + AOF (하이브리드 방식)
RDB + AOF 방식은 RDB와 AOF 방식의 장점을 결합한 방식으로, 두 가지 방식을 동시에 사용할 수 있도록 설정할 수 있습니다.
• 작동 원리:
• Redis는 주기적인 스냅샷을 생성하는 RDB 방식과, 모든 쓰기 명령을 기록하는 AOF 방식을 동시에 사용할 수 있습니다.
• 이를 통해 데이터의 안정성을 높이면서도 복구가 가능한 상태를 유지합니다.
• AOF는 데이터를 디스크에 쓰는 방식으로 안정성을 제공하고, RDB는 빠른 복구를 제공합니다.
• 장점:
• 최고의 데이터 안정성: AOF를 사용하여 거의 모든 쓰기 작업을 기록하고, RDB로는 빠른 복구를 제공할 수 있습니다.
• 유연한 복구: AOF가 디스크에 모든 명령을 기록하기 때문에 복구가 매우 정확하고, RDB는 빠른 복구를 지원합니다.
• 단점:
• 디스크 공간과 I/O 부담: RDB와 AOF 파일이 동시에 존재하므로 디스크 공간을 더 많이 차지하며, I/O 성능에 부담을 줄 수 있습니다.
• 서버 성능 저하: AOF와 RDB가 동시에 작동하므로 시스템 자원에 부담을 줄 수 있습니다.
Redis의 클러스터 모드는 어떻게 동작하며, 데이터 샤딩을 어떻게 처리하나요
Redis 클러스터 모드 (Redis Cluster Mode)
Redis 클러스터 모드는 여러 Redis 노드들(서버들)을 하나의 논리적인 Redis 데이터베이스 클러스터로 구성하는 방식입니다. 이를 통해 수평 확장(Horizontal Scaling)을 지원하고, 데이터의 분산 저장 및 복구를 자동화할 수 있습니다. Redis 클러스터는 데이터 샤딩(Sharding)을 활용하여 데이터를 여러 노드에 분산시키고, 각 노드는 자기 자신의 데이터만 저장합니다.
Redis 클러스터의 주요 특징
• 분산 저장: Redis 클러스터는 데이터를 여러 Redis 인스턴스에 분산하여 저장하고, 데이터의 일관성과 가용성을 보장합니다.
• 수평 확장: 노드를 추가하여 클러스터 용량을 확장할 수 있습니다.
• 내부 샤딩: Redis 클러스터는 자동으로 데이터를 샤딩하여 여러 노드에 분배합니다.
• 자동 장애 조치: Redis 클러스터는 장애 발생 시 자동으로 장애를 감지하고 다른 노드로 요청을 리디렉션하는 기능을 제공합니다.
데이터 샤딩 (Sharding)
데이터 샤딩은 데이터를 여러 Redis 노드에 분산하는 방식으로, 각 노드는 일부 데이터만 담당합니다. Redis 클러스터는 데이터를 해시 기반으로 분할하여 처리합니다.
Redis 클러스터에서의 샤딩
1. 키-값 쌍을 여러 슬롯으로 분할:
Redis 클러스터는 16,384개의 슬롯(Hash Slots)을 사용하여 데이터를 분할합니다. 각 데이터 키는 슬롯 번호를 계산하여 특정 Redis 노드에 할당됩니다.
• 각 클러스터 노드는 일부 슬롯만 담당하고, 데이터를 해당 슬롯 번호에 맞는 노드에 저장합니다.
• 예를 들어, key1에 대해 해시값을 계산하면, 그 값에 따라 해당 슬롯을 결정하고, 그 슬롯을 담당하는 노드에 key1 데이터를 저장합니다.
2. 슬롯 계산 방식:
• Redis는 CRC16 해시 알고리즘을 사용하여 키를 16,384개의 슬롯 중 하나에 매핑합니다.
• 예를 들어, key1을 해시한 후, 그 값이 slot1에 해당하면, slot1을 담당하는 Redis 노드에 key1이 저장됩니다.
3. 데이터 분배 및 재조정:
• Redis 클러스터는 각 노드에 슬롯을 할당합니다. 예를 들어, 노드 A는 슬롯 0에서 5000까지, 노드 B는 슬롯 5001에서 10000까지 데이터를 저장하는 식입니다.
• 만약 새로운 노드를 클러스터에 추가하면, 기존 노드들은 일부 슬롯을 이동하여 새로운 노드에 데이터를 분배합니다. 이는 자동 데이터 리밸런싱이라고 하며, 클러스터는 이를 자동으로 처리합니다.
Redis 클러스터에서의 요청 처리
1. 클라이언트가 요청을 보낼 때:
• 클라이언트는 Redis 클러스터의 任意 노드에 요청을 보낼 수 있습니다.
• 만약 요청이 데이터 키에 관련된 것이고, 해당 키의 슬롯이 다른 노드에 속한다면, 클러스터는 해당 노드로 리디렉션합니다.
• 리디렉션은 MOVED 또는 ASK 응답을 통해 이루어집니다. 클라이언트는 리디렉션 정보를 바탕으로 정확한 노드로 요청을 보냅니다.
2. MOVED vs ASK:
• MOVED: 요청이 잘못된 노드로 갔을 때, 클러스터가 해당 키가 위치한 정확한 노드의 주소를 알려줍니다. 클라이언트는 이 주소로 다시 요청을 보냅니다.
• ASK: 일부 상황에서 데이터가 리밸런싱 중일 때, 요청이 임시로 다른 노드로 리디렉션됩니다. 이 경우 클라이언트는 요청을 리디렉션된 노드로 보내야 하며, 이후 리밸런싱이 완료되면 정상적인 노드로 리디렉션됩니다.
3. 슬롯 이동 및 리밸런싱:
• Redis 클러스터가 크기가 커지고 데이터 분포가 불균형해지면, 새로운 노드를 추가하거나 일부 슬롯을 다른 노드로 이동시켜 리밸런싱을 할 수 있습니다.
• 이 과정에서 클러스터는 슬롯을 이동시키고, 기존 노드에 저장된 데이터를 새로운 노드로 이동시킵니다. Redis는 이를 자동으로 처리하며, 클러스터의 가용성을 유지합니다.
Redis 클러스터의 고가용성
Redis 클러스터는 마스터-슬레이브 구조를 통해 고가용성을 제공합니다. 각 마스터 노드는 하나 이상의 슬레이브 노드를 가질 수 있으며, 마스터 노드가 장애를 겪을 경우, 해당 마스터의 슬레이브 노드가 자동으로 승격되어 마스터 역할을 맡습니다.
• 자동 장애 조치: Redis 클러스터는 장애 발생 시, 장애가 발생한 마스터 노드의 슬레이브 노드를 자동으로 마스터로 승격시켜 장애를 처리합니다.
Redis 클러스터의 제한사항
• 멀티키 연산 제한: Redis 클러스터에서 멀티키 연산(예: MGET, MSET)은 같은 슬롯에 있는 키들만 처리할 수 있습니다. 다른 슬롯에 있는 키들에 대해 멀티키 연산을 시도하면 에러가 발생합니다. 이를 해결하기 위해 해시 태그를 사용할 수 있습니다.
• 해시 태그: 키에 {}를 사용하여 여러 키가 동일한 슬롯에 할당되도록 할 수 있습니다. 예를 들어, user:{1001}:profile와 user:{1002}:profile은 같은 슬롯에 할당됩니다.