프로젝트 예상 문제
1. Hibernate Lazy Initialization Exception의 원인과 해결 방법에 대한 질문
질문: Hibernate Lazy Initialization Exception이 발생하는 이유는 무엇인가요?
답변: LAZY로 설정된 엔티티는 실제로 참조될 때까지 로딩되지 않습니다. 그러나 세션이 종료된 후에 LAZY로 설정된 필드를 접근하려고 하면 Hibernate Lazy Initialization Exception이 발생합니다. 왜냐면, 세션이 닫힌 후에는 엔티티를 로드할 수 없기 때문입니다. 그래서 이 문제가 발생했을때 처음엔 EAGER 를 사용하여 해결했으나 이것을 사용하면 성능에 문제가 생길수가 있어서 Hibernate5Module, JsonIgnoreProperties 등도 고려해 보았으나 dto를 사용해 해결했습니다.
2. EAGER 페치 전략을 사용하면 성능 문제가 발생할 수 있다고 했는데, 그 이유는 무엇인가요?
답변: EAGER 페치 전략은 해당 엔티티와 연관된 모든 엔티티를 즉시 로드합니다. 따라서 불필요한 데이터까지 한 번에 모두 로딩하게 되며, 특히 연관된 엔티티들이 많을 경우 성능에 큰 부담을 줄 수 있습니다. 이는 데이터베이스 쿼리의 성능을 저하시켜, 특히 많은 양의 데이터를 처리할 때 응답 시간이 길어질 수 있습니다.
3. DTO를 사용한 해결 방법에 대한 질문
질문: DTO를 사용해서 Lazy Initialization Exception 문제를 해결했다고 했는데, 이 방식의 장점은 무엇인가요?
답변: DTO를 사용하면 필요한 필드들만 선택적으로 조회할 수 있기 때문에, 세션이 열려 있는 동안 필요한 데이터만 로드하고, 세션이 종료된 후에도 필요한 정보만 접근할 수 있습니다. 이는 EAGER처럼 모든 연관 데이터를 로딩하지 않으면서도, 필요한 데이터는 세션 종료 전에 미리 로드할 수 있다는 장점이 있습니다.
4. Lazy 로딩 상태에서 DTO를 사용했을 때, 엔티티를 직접 반환하는 것보다 성능이 더 나은 이유는 무엇인가요?
답변: 엔티티를 직접 반환하면 모든 연관된 데이터가 필요할 때 즉시 로드되기 때문에, 불필요한 데이터까지 메모리에 로드될 수 있습니다. 그러나 DTO는 필요한 데이터만 명시적으로 로드하여 메모리 사용을 최소화할 수 있고, 데이터베이스에 불필요한 쿼리가 발생하는 것을 줄일 수 있어 성능상 이점이 있습니다.
5. 다른 해결방법은 왜 사용 안했는지
질문: Hibernate5Module을 도입하는 방법을 검토하셨다고 했는데, 이 방법의 장점과 단점은 무엇인가요?
답변: Hibernate5Module을 사용하면 Lazy 로딩된 엔티티를 세션이 종료된 후에도 직렬화할 수 있기 때문에, Lazy Initialization Exception 문제를 해결할 수 있습니다. 그러나 Hibernate5Module을 추가하려면 build.gradle에 관련 설정을 추가해야 하고, 엔티티 직렬화에 관련된 추가 설정이 필요하기 때문에 복잡성이 증가할 수 있습니다. 따라서 단순한 프로젝트에서는 불필요하게 복잡해질 수 있어 선택하지 않았습니다.
질문: @JsonIgnoreProperties를 사용해 문제를 해결할 수 있었는데, 왜 반려하셨나요?
답변: @JsonIgnoreProperties를 사용하면 LAZY 로딩된 필드를 직렬화할 때 무시할 수 있어 예외는 방지할 수 있습니다. 하지만 이 방법은 LAZY 로딩의 장점을 살리지 못하고, 해당 필드를 무시하는 방식이기 때문에 실제 필요한 데이터까지 무시될 수 있습니다. 따라서 LAZY 로딩을 사용하는 의미가 없어져 적합하지 않다고 판단했습니다.
6. 페이징과 lazy
질문: 페이징 처리와 Lazy 로딩이 함께 사용될 때, 성능 이슈는 어떻게 관리하셨나요?
답변: 페이징 처리를 할 때도 DTO를 사용하여 필요한 데이터만 조회하고, Lazy 로딩으로 인한 성능 문제를 줄이기 위해 필요한 필드만 로드했습니다. 이렇게 하면 대량의 연관 데이터를 한꺼번에 로드하지 않고, 페이징된 데이터를 효율적으로 가져올 수 있습니다.
7. 소셜 로그인과 일반 로그인 통합 관련 질문
- 질문: 소셜 로그인에서 providerId와 userId가 다른데, 이 차이를 어떻게 처리하셨나요?
- 답변: 소셜 로그인에서는 각 소셜 서비스(카카오, 구글 등)가 발급하는 providerId를 사용하여 사용자 계정을 관리합니다. 일반 로그인에서는 userId를 사용하지만, 소셜 로그인의 경우 providerId를 추가적으로 관리해야 합니다. 처음에는 userPrincipal에 providerId 필드를 추가하여 이 문제를 해결하려 했습니다. 하지만 더 효율적인 방식으로 서비스 계층에서 providerId를 기준으로 데이터베이스에서 사용자 정보를 조회하도록 로직을 수정했습니다.
8. JWT 관련 질문
- 질문: JWT 토큰에 providerId를 포함해 문제를 해결한 방식에 대해 설명해 주세요.
- 답변: 기존에 JWT 토큰에는 userId만 포함되었는데, 소셜 로그인 사용자를 위해 providerId도 JWT 페이로드에 포함시키는 방법을 사용했습니다. 이렇게 하면, JWT 토큰에서 providerId를 추출하여 해당 사용자 정보를 조회할 수 있게 됩니다. 따라서 JWT 토큰의 payload 부분에서 providerId를 추출하고 이를 UserPrincipal에 반영하여 인증 과정을 처리하게 만들었습니다.
9. NullPointerException 발생 원인 및 해결 방법
- 질문: NullPointerException이 발생한 원인은 무엇이며, 이를 어떻게 해결했나요?
- 답변: NullPointerException은 providerId가 null인 경우 발생했습니다. JWT 토큰에서 providerId를 정상적으로 가져오지 못하는 상황에서 발생한 것으로, 이는 JWT 페이로드에 providerId 필드가 포함되지 않았기 때문입니다. 이를 해결하기 위해, JWT 생성 시 providerId 필드를 포함하도록 수정하였고, JWT 토큰 생성 시 사용자의 providerId를 포함시키는 로직을 추가하여 문제를 해결했습니다.
10. UserPrincipal 변경과 트러블슈팅 과정에 대한 질문
- 질문: UserPrincipal에 providerId를 추가한 후에도 문제가 발생했는데, 그 이유는 무엇인가요?
- 답변: 처음에는 UserPrincipal에 providerId 필드를 추가하는 것으로 문제가 해결될 것이라고 생각했습니다. 하지만, 단순히 UserPrincipal만 변경해서는 모든 흐름이 변경되지 않았습니다. 인증 과정에서 JWT 토큰에서 추출한 providerId로 사용자를 조회해야 했기 때문에, Service 계층에서 사용자 조회 로직을 providerId 기반으로 수정하였습니다. 즉, findByProviderId 메서드를 추가해 providerId를 기반으로 사용자를 조회하게 하여 문제를 해결했습니다.
11. 트러블슈팅 과정에서의 판단과 최종 해결책
- 질문: 처음 시도한 해결 방법과 최종 선택한 해결 방법의 차이점은 무엇인가요?
- 답변: 처음에는 UserPrincipal에 providerId를 추가하는 방식으로 문제를 해결하려 했지만, UserPrincipal 자체를 변경하는 것만으로는 충분하지 않았습니다. 결국 서비스 계층의 로직을 수정하여 userId 대신 providerId로 사용자를 조회하는 방식으로 변경하는 것이 더 간단하고 효과적이라는 결론에 도달했습니다. 최종적으로, JWT 토큰에서 providerId를 추출하고, 이를 기반으로 서비스 계층에서 조회하도록 로직을 수정하여 문제를 해결했습니다.
기술문제
1. Kotlin 언어가 다른 언어가 가장 큰 차이점을 가지는 점은 무엇이라고 생각하는가?
Kotlin의 가장 큰 차이점은 null 안정성이 높고, 간결한 문법을 가진것이라고 생각합니다.
예를 들어, 게터와 세터, 생성자를 줄여서 프로그래머가 그걸 직접 작성하지 않아도 되고, 변수가 null을 허용하는지 아닌지 명시하며, 기존의 자바 코드를 호환할 수 있는것이 큰 차이점이자 장점입니다.
2. JPA에서 제공되는 QueryMethod명이 길어지는 경우, 유지보수에 불편함이 있는데, 다른 대안은 없는가?
QueryMethod명이 길어지는걸 해결하기 위한 방법으론 두가지가 있는데, @Query 어노테이션을 사용하여 JPQL로 직접 쿼리를 작성하거나, Querydsl을 사용하는 방법이 있습니다.
두 방법 모두 유지보수성을 개선하고 가독성을 높이는 대안으로 적합하지만, 상황에 따라 간단한 쿼리는 @Query로, 복잡한 동적 쿼리는 Querydsl로 해결하는 것이 좋습니다.
3. @Valid 어노테이션의 역할과 특징, 실행 프로세스에 대해서 설명해주세요
@Valid 어노테이션은 입력 데이터의 유효성을 검증하는 데 사용됩니다. 주로 DTO나 입력 폼을 통해 전달된 데이터를 자동으로 검증하는 역할을 합니다. 이를 통해 필드 단위로 정의된 제약 조건에 맞는지를 확인하고, 맞지 않을 경우 예외를 발생시킵니다.
@Valid의 주요 특징은, 객체 내부에 설정된 유효성 검사 어노테이션들을 기반으로 검증을 수행한다는 점입니다. 예를 들어, 특정 필드에 @NotNull, @Size, @Email 같은 어노테이션을 설정하면, 유효성 검사가 자동으로 수행됩니다.
이를 컨트롤러에서 @Valid 어노테이션을 사용해 DTO를 받을 때 검증이 이루어지며, 유효성 검증에 실패하면 스프링이 자동으로 예외를 발생시킵니다. 예외 처리 방식은 스프링의 전역 예외 처리기를 사용하거나, BindingResult를 활용하여 오류 메시지를 커스터마이징할 수 있습니다.
4. 소프트 딜리트와 하드 딜리트에 대한 정의와 언제 어떤 딜리트 방식을 써야 하는지 본인의 생각을 이야기 해주세요.
소프트 딜리트는 데이터를 실제로 삭제하지 않고, '삭제됨'이라는 상태를 표시해 데이터베이스에 여전히 남겨두는 방식이고, 하드 딜리트는 데이터 자체를 완전히 삭제하는 방식입니다.
이 두 방식은 데이터 복구 가능성, 비즈니스 요구사항에 따라 선택적으로 사용되며, 주로 데이터의 중요성과 보존 기간에 따라 결정됩니다.
소프트 딜리트는 데이터가 필요하지 않거나 삭제되었지만, 나중에 복구해야 할 가능성이 있거나 기록이 필요한 경우에 주로 사용됩니다. 예를 들어, 사용자 데이터를 삭제할 때, 해당 사용자가 다시 복구를 요청할 수 있거나 삭제 기록이 남아야 하는 비즈니스 요구사항이 있을 때 유용합니다.
반면에, 하드 딜리트는 데이터가 완전히 삭제되어 복구할 수 없는 방식입니다. 주로 민감한 데이터가 포함되었거나, 데이터를 더 이상 보관할 필요가 없는 경우에 사용합니다. 예를 들어, 정책상 데이터가 영구적으로 삭제되어야 하거나, 법적인 이유로 데이터가 보관되면 안 되는 경우 하드 딜리트가 적합합니다.
결론적으로, 소프트 딜리트는 데이터 복구나 이력 추적이 필요한 경우에 유용하며, 주로 사용자 관리, 로그 관리 등에서 사용됩니다. 반면에, 하드 딜리트는 민감한 데이터나 법적 규제에 따라 삭제가 요구되는 상황에서 더 적합합니다.
5. 대용량 데이터를 DB에 저장할 때 고려해야 할 사항은 무엇이 있다고 생각하나요?
대용량 데이터를 DB에 저장할 때는 성능, 확장성, 데이터 무결성, 그리고 백업 및 복구 전략과 같은 여러 가지 측면을 종합적으로 고려해야 합니다.
첫 번째로 성능이 중요한 고려 사항입니다. 대용량 데이터가 쌓이면 쿼리 성능이 저하될 수 있기 때문에, 인덱스, 파티션 같은 기법을 사용해 데이터 접근 속도를 최적화해야 합니다. 특히, 자주 조회되는 필드에 적절한 인덱스를 설정하고, 테이블을 파티셔닝하여 데이터를 나눠서 관리하면 성능이 크게 향상됩니다.
두 번째로는 확장성입니다. 데이터가 지속적으로 증가할 것을 예상하고, 수평적 확장을 고려할 필요가 있습니다. 데이터베이스를 수직적 확장(성능을 높이는 하드웨어 추가)만으로는 한계가 있기 때문에, 데이터를 여러 노드에 나누어 저장하는 Sharding 기법이 유용할 수 있습니다. 이렇게 하면 시스템의 부하를 분산시킬 수 있습니다.
세 번째로는 데이터 무결성입니다. 대용량 데이터를 다루면서도 ACID 속성을 유지하려면 트랜잭션 관리를 신경 써야 합니다. 동시에, 데이터의 정합성을 보장하기 위해 Batch 처리나 비동기 처리와 같은 기법을 도입할 수 있습니다. 대량의 데이터를 한 번에 처리할 경우 데이터 정합성을 고려해야 합니다.
마지막으로 백업 및 복구 전략도 고려해야 합니다. 대용량 데이터는 백업과 복구가 오래 걸릴 수 있기 때문에, 증분 백업이나 지속적인 데이터 백업을 위한 시스템을 설계해야 합니다. 이를 통해 예상치 못한 장애에 대비하고 데이터 손실을 최소화할 수 있습니다.
결론적으로, 대용량 데이터를 DB에 저장할 때는 성능 최적화, 확장성, 데이터 무결성, 그리고 백업 및 복구 전략을 철저히 고려해야 합니다.
6. 데이터베이스의 정규화 작업에 대해서 설명해주세요
데이터베이스 정규화는 데이터를 중복 없이 효율적으로 관리하기 위해 데이터베이스 테이블을 구조화하는 과정입니다. 이를 통해 데이터의 무결성을 보장하고, 데이터 중복으로 인한 불필요한 저장 공간 낭비를 방지하며, 삽입, 갱신, 삭제 시의 이상 현상을 최소화할 수 있습니다.
정규화의 주요 단계는 1NF, 2NF, 3NF로 나뉩니다. 각 단계에서 데이터의 구조가 점진적으로 개선됩니다.
1차 정규화 (1NF): 이 단계는 모든 필드가 원자값을 가지도록 테이블을 설계하는 것입니다. 즉, 테이블의 각 열에 하나의 값만 들어가게 하여, 데이터의 원자성을 보장합니다. 이를 통해 반복되는 데이터를 방지할 수 있습니다.
예시로, 복수의 전화번호가 한 칸에 들어가는 것을 방지하고, 별도의 레코드로 나누는 것이 1차 정규화입니다.
2차 정규화 (2NF): 부분적 종속성을 제거하는 단계입니다. 기본 키의 일부에만 의존하는 필드를 별도의 테이블로 분리하여, 각 필드가 완전 함수 종속을 이루도록 합니다. 이렇게 하면 데이터 중복을 줄이고 삽입/삭제 시의 이상 현상을 방지할 수 있습니다.
예를 들어, 학생의 수업 정보를 관리할 때, 학생과 수업 간의 종속성을 분리해 테이블을 나누는 것이 2차 정규화의 목표입니다.
3차 정규화 (3NF): 이행적 종속성을 제거하는 단계입니다. 즉, 기본 키에 간접적으로 의존하는 속성을 분리하여, 모든 비키 속성이 기본 키에만 의존하도록 만듭니다. 이를 통해 데이터의 일관성을 유지하고, 불필요한 데이터 수정을 방지할 수 있습니다.
예를 들어, 학생의 학과 이름이 학과 코드에 의존하는 경우, 학과 코드를 별도의 테이블로 분리하여 중복된 정보를 제거하는 것이 3차 정규화입니다.
"정규화를 통해 데이터베이스는 중복된 데이터를 최소화하고, 데이터의 무결성을 유지하면서도 효율적인 쿼리 성능을 기대할 수 있습니다."
3. 마무리:
결론적으로, 정규화는 데이터 중복을 줄이고 데이터 무결성을 유지하는 중요한 데이터베이스 설계 기법입니다. 이를 통해 삽입, 갱신, 삭제 시 발생할 수 있는 이상 현상을 최소화하고, 데이터베이스의 성능을 최적화할 수 있습니다. 다만, 지나치게 정규화를 하면 조인 연산이 많아져 성능에 영향을 줄 수 있기 때문에, 정규화와 비정규화의 균형을 맞추는 것이 중요합니다.