
Eager Loading
ManyToOne 관계 설정 시의 기본 값
-장점:
데이터베이스에서 한 번의 쿼리로 필요한 데이터를 모두 가져오므로 접근 속도가 빠릅니다.
트랜잭션 내에서 필요한 모든 데이터가 이미 로딩되어 있어 안전합니다.
-단점:
필요하지 않은 데이터까지 모두 로딩하므로 메모리 사용량이 많아질 수 있습니다.
초기 로딩 시간이 길어질 수 있습니다.
Lazy Loading
OneToMany 관계 설정 시의 기본 값
-장점:
초기 로딩 시 필요한 데이터만 가져오기 때문에 메모리 사용량이 적습니다.
초기 로딩 속도가 빠릅니다.
-단점:
관련된 엔티티를 실제로 사용할 때마다 추가 쿼리가 발생할 수 있습니다.
트랜잭션 외부에서 접근 시 LazyInitializationException이 발생할 수 있습니다.
차이점
가장 큰 차이점은 연관관계 맵핑된 Entity의 로드되는 시점
정의: 하나의 쿼리로 N개의 엔티티를 조회한 후, 각 엔티티와 연관된 데이터를 조회하기 위해 추가로 N번의 쿼리가 실행되는 문제를 말합니다. 결과적으로 총 N+1번의 쿼리가 실행되어 성능에 큰 영향을 미칩니다.
→ 데이터 개수만큼 엔티티 연관관계 조회 쿼리가 발생하는 현상
해결방법:
-Fetch Join:
하나의 쿼리로 연관된 엔티티를 함께 조회
예: SELECT p FROM Parent p JOIN FETCH p.children
장점: 쿼리 수가 줄어들어 성능이 개선됩니다.
단점: 너무 많은 데이터를 한 번에 로딩할 경우 메모리 사용량이 많아질 수 있습니다.
Batch Fetching: (배치 사이즈 조절)
한번에 여러 연관된 엔티티를 배치로 가져오는 방식입니다.
JPA 설정에서 @BatchSize를 사용하여 일괄 로딩 크기를 지정할 수 있습니다.
예: @BatchSize(size = 10)
-Entity Graph:
엔티티 그래프를 정의하여 특정 조회 시점에 필요한 연관 엔티티를 명시적으로 지정합니다.
예: @EntityGraph(attributePaths = {"children"})
장점: 동적으로 필요한 데이터를 선택적으로 로딩할 수 있습니다.
실제로는 한가지 방법을 택하기 보다는 지연로딩(LAZY) 사용하고 성능 최적화에서 Fetch조인을 사용하고 Batch size를 1000이하의 값으로 연관관계는 꼭 필요할때만 사용하는게 좋다.
통합 테스트와 단위 테스트의 차이점
단위 테스트:
정의: 애플리케이션의 가장 작은 단위(보통 메서드나 클래스)를 독립적으로 테스트하는 것.
목적: 특정 기능이 올바르게 작동하는지 확인.
특징:
다른 컴포넌트와 독립적으로 테스트.
Mock 객체를 많이 사용.
빠른 실행 속도.
개발 초기에 작성 (TDD : 테스트 코드를 먼저 작성하는 개발 방법론)
해당 부분만 독립적으로 테스트하기 때문에 어떤 코드를 리팩토링하여도 빠르게 문제 여부를 확인할 수 있다.
테스팅에 대한 시간과 비용을 절감할 수 있다.
새로운 기능 추가 시에 수시로 빠르게 테스트 할 수 있다.
리팩토링 시에 안정성을 확보할 수 있다.
코드에 대한 문서가 될 수 있다.
또한 좋고 깨끗한 테스트 코드는 FIRST라는 5가지 규칙을 따라야 한다.
Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 한다.
Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안된다.
Repeatable: 어느 환경에서도 반복 가능해야 한다.
Self-Validating: 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 자체적으로 검증되어야 한다.
Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.
통합 테스트:
정의: 여러 단위가 결합된 기능이나 모듈이 올바르게 상호작용하는지 테스트하는 것.
최소 두개 이상의 단위가 잘 작동하는 지, 개발 후반부에 시행합니
목적: 시스템의 각 부분이 함께 제대로 작동하는지 확인.
특징:
실제 환경과 비슷한 조건에서 테스트.
데이터베이스, 네트워크 등 외부 시스템과의 통합 테스트 포함.
상대적으로 느린 실행 속도.
단위 테스트와 통합 테스트의 장/단점
단위 테스트:
장점:
빠른 피드백: 코드 변경 후 즉각적인 피드백 가능.
디버깅 용이: 특정 기능만 테스트하므로 문제를 쉽게 찾아낼 수 있음.
코드 리팩토링: 코드 변경 시 안정성을 보장.
단점:
전체 시스템의 동작 보장 어려움: 개별 단위가 올바르게 작동해도 통합 시 문제가 발생할 수 있음.
Mocking의 필요성: 실제 환경과 다를 수 있음.
통합 테스트:
장점:
시스템 전반의 동작 확인: 실제 사용 환경과 유사한 조건에서 테스트.
외부 시스템과의 통합 확인: 데이터베이스, 네트워크 등과의 상호작용 테스트 가능.
단점:
느린 실행 속도: 실제 시스템과의 통합 때문에 시간 소요.
디버깅 어려움: 문제 발생 시 원인 파악이 어려울 수 있음.
레이어별로 나누어서 Slice Test를 하는 이유
Slice Test:
특정 레이어(Controller, Service, Repository 등)에 대한 테스트를 수행하여 각 레이어의 동작을 검증하는 테스트 방법.
@WebMvcTest
@WebFluxTest
@DataJpaTest
@JsonTest
@RestClientTest
각 레이어의 독립적인 테스트를 진행하면 기능 구현과 동시에 테스트를 하기에 간 편해집니다. 스프링 설정을 따로 해 줄 필요도 없을뿐더러, DB 연결도 필요 없고, 테스트할 때마다 빈을 로드하지 않아도 되어 빠른 속도로 테스트를 할 수 있게 됐습니다. 또한, 테스트 할 때 불필요한 정보들이 줄어들면서 테스트 코드의 가독성이 올라가게 됐습니다.
신속한 피드백: 단위 테스트처럼 빠르게 실행되어 신속한 피드백을 제공.
격리된 환경: 특정 레이어만 테스트하므로 다른 레이어의 영향을 받지 않고 독립적으로 테스트 가능.
문제 파악 용이: 각 레이어별로 테스트하므로 문제가 발생한 레이어를 쉽게 파악할 수 있음.
효율적인 리소스 사용: 통합 테스트에 비해 필요한 리소스가 적고, 설정이 간단함.
이유:
테스트 코드 작성의 필요성
코드 안정성 확보: 변경 시 기존 기능이 올바르게 작동하는지 확인할 수 있어 안정성을 높임.
디버깅 용이성: 버그 발생 시 빠르게 원인을 파악하고 수정 가능.
자동화 가능: 지속적인 통합(CI) 및 배포(CD) 과정에서 자동화된 테스트 실행으로 품질 보장.
문서화 역할: 테스트 코드는 코드의 사용 방법과 예상 결과를 보여주는 문서화 역할을 함.
리팩토링 지원: 기존 코드의 동작을 보장하면서 리팩토링할 수 있어 유지보수에 용이.
테스트 코드를 작성하면서 느낀 필요성:
JPA와 Hibernate의 차이점
정의: 자바에서 ORM(Object-Relational Mapping)을 위한 표준 인터페이스.
역할: 애플리케이션과 데이터베이스 사이의 데이터 매핑을 위한 표준을 제공.
구현체: Hibernate, EclipseLink, OpenJPA 등이 JPA의 구현체.
ORM즉, 객체가 테이블이 되도록 매핑 시켜주는 것을 말합니다.생산성이 매우 높아집니다.그래서 나중에 다루게 될 JPQL, QueryDSL 등을 사용하거나 한 프로젝트 내에서 Mybatis와 JPA를 같이 사용하기도 합니다.
영속성 컨텍스트(persistence context) : 엔티티를 영구 저장하는 환경 데이터베이스와 영속성 컨텍스트를 통해 연결되있는 엔티티(객체)
Hibernate:
정의: JPA를 구현한 ORM 프레임워크 중 하나.
역할: JPA의 표준을 따르며, 추가적인 기능(캐싱, 데이터베이스 벤더별 기능 등)을 제공.
특징: 다양한 기능과 유연성을 제공하여 JPA를 사용하는 개발자들에게 자주 선택됨.
spring starter - data jpa 의존성에 이미 구현체가 포함되어있어 jpa와 호환이 잘된
객체에 비지니스 책임을 위임할 수 있어요.
객체를 불러올 때, 연관된 객체 또한 함께 불러오기 때문에,
SQL에 의존적이지 않은 개발을 할 수 있어요.
즉, SQL 중심이 아닌 객체 중심의 개발이 가능해요
그러나 query가 복잡해지면 ORM으로 표현하는데 한계가 있고, 성능이 raw query에 비해 느리다는 단점이 있는데요,
ORM을 이용하면 SQL Query가 아닌 직관적인 코드(메서드)로서 데이터를 조작할 수 있습니다.
ORM이란 객체와 DB의 테이블이 매핑을 이루는 것을 말합니다. (Java 진영에 국한된 기술은 아닙니다.) - 애플리케이션과 데이터베이스 사이의 중간계층 - 트랜잭션이 완료되면 데베에 반
JPA (Java Persistence API):
QueryDSL을 사용하여 복잡한 동적 쿼리를 작성하는 방법
기본 설정:
build.gradle에 QueryDSL 관련 의존성 추가:
groovy코드 복사 dependencies { implementation 'com.querydsl:querydsl-jpa' annotationProcessor 'com.querydsl:querydsl-apt' }
Q 클래스 생성:
엔티티 클래스를 기반으로 Q 클래스를 생성.
예: QMember qMember = QMember.member;
쿼리 작성:
JPAQueryFactory를 사용하여 동적 쿼리를 작성:
java코드 복사 JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager); List members = queryFactory.selectFrom(QMember.member) .where(QMember.member.age.gt(30)) .fetch();
동적 조건 추가:
BooleanBuilder를 사용하여 조건을 동적으로 추가:
java코드 복사 BooleanBuilder builder = new BooleanBuilder(); if (age != null) { builder.and(QMember.member.age.gt(age)); } if (name != null) { builder.and(QMember.member.name.eq(name)); } List members = queryFactory.selectFrom(QMember.member) .where(builder) .fetch();
프로젝트에서 좋아요 기능을 구현할 때, 특정 사용자가 특정 게시글을 이미 좋아요 했는지 확인하는 방법
엔티티 설정:
User와 Post 엔티티 사이에 Like 엔티티를 만들어 다대다 관계를 매핑.
예:
java코드 복사 @Entity public class Like { @Id @GeneratedValue private Long id; @ManyToOne private User user; @ManyToOne private Post post; }
레포지토리 메서드 작성:
특정 사용자가 특정 게시글을 좋아요 했는지 확인하는 쿼리 작성:
java코드 복사 public interface LikeRepository extends JpaRepository<Like, Long> { boolean existsByUserAndPost(User user, Post post); }
서비스에서 사용:
서비스 클래스에서 레포지토리 메서드를 사용하여 좋아요 상태 확인:
java코드 복사 @Service public class LikeService { @Autowired private LikeRepository likeRepository; public boolean isLikedByUser(User user, Post post) { return likeRepository.existsByUserAndPost(user, post); } }
이렇게 하면 특정 사용자가 특정 게시글을 이미 좋아요 했는지 쉽게 확인할 수 있습니다.
트랜잭션이 뭐고 왜 필요한지, 예시를 들어서 설명해주세요
정의: 데이터베이스의 상태를 변화시키는 작업의 논리적 단위
특징 (ACID): 원자/일관/독립/영속
Atomicity: 모든 작업이 완전히 성공하거나 완전히 실패해야 합니다.
Consistency: 트랜잭션이 완료되면 데이터베이스가 일관성 있는 상태로 유지됩니다.
Isolation: 트랜잭션은 다른 트랜잭션으로부터 독립적으로 실행되어야 합니다.
Durability: 트랜잭션이 성공하면 그 결과는 영구적으로 반영됩니다.
필요성:
데이터 일관성 유지
오류 발생 시 데이터 복구 가능
동시에 여러 사용자 접근 시 데이터 무결성 보장
예시:
은행 계좌 이체: A 계좌에서 B 계좌로 돈을 이체하는 경우, A 계좌에서 출금과 B 계좌로 입금이 모두 성공해야 합니다. 둘 중 하나라도 실패하면 전체 작업이 취소되어야 합니다.
코드에서 사용:
트랜잭션 어노테이션 사용:
java코드 복사 @Transactional public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) { // 출금 로직 // 입금 로직 }
어노테이션 동작 원리:
@Transactional 어노테이션이 붙은 메서드는 프록시 객체에 의해 트랜잭션이 시작되고 종료됩니다.
트랜잭션이 시작될 때 데이터베이스 커넥션이 생성되고, 메서드 실행이 완료되면 커밋 또는 롤백됩니다.
예외가 발생하면 트랜잭션이 롤백되고, 성공적으로 완료되면 커밋됩니다
<엔티티의 생명주기 4가지>
: 엔티티를 영구 저장하는 환경, 논리적인 개념
: 애플리케이션 - DB 사이에서 객체를 보관하는 가상의 DB 역할 (→ 플러시 시점에 DB에 반영)
비영속 상태 (new/transient)
객체만 생성하고 엔티티와 연결X
영속 상태 (managed) → @Id로 매핑된 키값이 있어야함
객체를 생성하고 저장한 상태,
영속성 컨텍스트가 관리하는 엔티티(1차캐시/쓰기지연저장소에 있는 상태)
ex) em.persist(member);
//객체를 생성만 한 상태(비영속)
Member member = new Member();
member.setId("member1");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//객체를 저장한 상태(영속)
em.persist(member);
준영속 상태 (detached)
엔티티가 영속성 컨텍스트에서 분리 ex) em.detached( );
삭제 상태 (removed)
엔티티 삭제 ex) em.remove( );