🚨 트러블 슈팅 목록 (~1/24)
- 다중 LEFT JOIN 시
MultipleBagFetchException 발생 문제
- docker hub을 이용해서 CD환경 구축했을때, 최신 내용이 적용 안되는 문제
- 찜하기 api에서 나온 race condition
- 멀티-서버 프로젝트에서 gradle 설정
- common 프로젝트 의존에서 jar 순환참조 문제
- common 프로젝트에 등록한 엔티티의 의존을 받지 못하는 문제
@Scheduled(fixedDelay=) 어노테이션을 이용했는데 스케쥴링이 안되는 문제
- repository test 작성시
@DataJpaTest , @Transactional 사용하게 된 과정
- Test시 의존받는 bean를 로드하지 못하는 문제
- user테이블과 같이 sql의 예약어랑 이름이 겹칠때 생기는 문제
- test에서
@Autowired 어노테이션을 이용시 인스턴스의 상태공유가 안되는 문제
- pin삭제시 연관된 user, house도 casecade되어 삭제되는 문제
- 지연 로딩하려 할 때 세션/엔티티매니저가 닫혀 LazyInitializationException이 발생하는 문제
- DTO를
Record로 변환하고 하나의 파일로 통합한 과정
- swap 메모리로 메모리 부족 해결
다중 LEFT JOIN 시 MultipleBagFetchException 발생 문제
문제
테스트 중, 다음과 같은 쿼리에서 MultipleBagFetchException이 발생함:
@Query("SELECT h FROM House h " +
"LEFT JOIN FETCH h.rooms r " +
"LEFT JOIN FETCH r.roommates " +
"LEFT JOIN FETCH h.pins " +
"WHERE h.id = :houseId")
Optional<House> findHouseDetailsById(@Param("houseId") Long houseId);
MultipleBagFetchException은 Hibernate에서 여러 개의 @OneToMany 관계를 fetch join할 경우 발생하는 문제로, 한 번의 쿼리로 여러 개의 컬렉션(List)을 동시에 로드하려고 시도하면 발생함.
House에는 rooms, pins, recentlyViewedHouses 세 가지 @OneToMany 관계가 있었는데, 위의 쿼리는 rooms, pins와 rooms 내의 roommates를 모두 fetch join하여 한 번의 쿼리로 데이터를 가져오려고 시도하였기 때문에 오류가 발생하였다.
Hibernate는 SQL에서 결과를 하나의 Cartesian Product로 생성하고, 이를 Java 컬렉션으로 매핑하는 방식으로 작동한다. 하지만 여러 개의 컬렉션을 동시에 fetch join할 경우, 이 결과를 적절히 분해하지 못해 문제가 발생하는 것이다.
Cartesian Product는 두 테이블 간의 모든 가능한 조합을 반환하는 방식이다.
어떤 조인 조건도 사용하지 않고, 두 테이블의 모든 행과 열을 조합하여 결과를 생성한다. 결과는 두 테이블의 행 수를 곱한 만큼의 행을 가지게 된다. (만약 A와 B 테이블의 행이 10개씩 있다면 카티시안 곱의 결과는 100행)행이 모든 경우의 수가 들어가 필요없는 행까지 포함되므로, 조건절을 지정하여 사용할 수 있다.
일반적으로는 카티시안 곱 보다는 Join문들을 많이 사용한다.
출처: https://sjh9708.tistory.com/173
- Hibernate 제약:
- 한 번의 쿼리로 여러 개의
@OneToMany 컬렉션을 fetch join하면, 결과 집합이 중복되거나 불필요한 Cartesian Product가 생성됨. Hibernate는 이를 적절히 처리하지 못해 MultipleBagFetchException을 던짐.
- 쿼리의 과도한 데이터 로드:
- 필요한 데이터만 로드하면 되지만, 여러 관계를 한 번에 가져오려다 보니 성능상 비효율적이고 복잡한 쿼리가 생성됨.
- 이는 네트워크 부하 및 쿼리 성능 문제로 이어질 가능성도 있었음.
해결
모든 관계를 한 번에 fetch join하지 않도록 쿼리를 분리
✋🏻 여기서 잠깐
Fetch Join / Left Join 설명
1. Fetch Join
Fetch Join은 Hibernate와 같은 ORM에서 엔티티와 연관된 컬렉션이나 엔티티를 즉시 로딩(Eager Loading) 방식으로 가져오기 위해 사용하는 기법이다.
- Fetch Join의 특징:
- 연관된 엔티티나 컬렉션을 한 번의 쿼리로 함께 조회한다.
- SQL의
JOIN과는 다르게, 엔티티 그래프(Entity Graph)를 로드하는 데 사용된다.
- JPQL에서
JOIN FETCH 키워드를 사용하여 명시적으로 설정할 수 있다.
- 장점:
- N+1 문제를 방지할 수 있다.
- 필요한 데이터를 한 번의 쿼리로 가져오기 때문에 성능상 유리하다.
- 단점:
- 하나의 쿼리에서 여러 컬렉션을 fetch join하면
MultipleBagFetchException이 발생할 수 있다.
- 불필요한 데이터까지 로드하게 되어 성능 저하를 초래할 수 있다.
2. Left Join
Left Join은 SQL에서 사용하는 조인 방식으로, 기준 테이블의 모든 데이터와 조인된 테이블의 일치하는 데이터를 결합하여 결과를 반환한다.
- Left Join의 특징:
- 기준 테이블에 조인된 테이블의 데이터가 없더라도, 기준 테이블의 모든 데이터가 포함된다.
- 조인된 테이블에 데이터가 없을 경우
NULL 값이 들어간다.
- 장점:
- 조인된 테이블의 데이터가 없더라도 기준 테이블의 데이터를 유지할 수 있다.
- 조인된 테이블 데이터의 존재 여부에 따라 유연하게 쿼리 결과를 처리할 수 있다.
- 단점:
- 불필요한
NULL 값을 포함한 데이터가 반환될 수 있어, 클라이언트에서 이를 처리해야 한다.
Fetch Join과 Left Join의 차이
| 항목 |
Fetch Join |
Left Join |
| 목적 |
엔티티와 연관된 데이터를 한 번의 쿼리로 즉시 로드 |
SQL 방식으로 테이블을 조인 |
| 결과 반환 |
엔티티 그래프로 반환 |
테이블 데이터를 조합하여 결과를 반환 |
| 사용 위치 |
ORM(JPA, Hibernate) |
SQL 및 JPQL |
| Lazy Loading 영향 |
Lazy Loading 설정을 무시하고 즉시 로드 |
Lazy Loading 설정을 따르며, 조인된 데이터를 지연 로딩할 수 있음 |
| N+1 문제 해결 |
해결 가능 |
해결 불가능(N+1 발생 가능) |
Docker hub을 이용해서 CD환경 구축했을때, 적용한 최신 내용이 적용 안되는 문제
원인
CD를 위해 docker hub으로 images를 push는 하고 있었으나, pull하고 docker를 실행시키지 않았다.
아래 명령어로는 기존에 이미지가 없을때는 이미지를 pull 하나, 기존에 이미지가 있을땐 pull을 하지 않는 문제다.
echo "Running Producer container"
sudo docker run -d --name producer -p 8080:8080 \
--env-file .env \
${{ secrets.DOCKER_USERNAME }}/roomie-producer:latest
echo "Running Consumer container"
sudo docker run -d --name consumer -p 8081:8081 \
--env-file .env \
${{ secrets.DOCKER_USERNAME }}/roomie-consumer:latest
해결
docker images 명령어를 사용하면 보유 이미지와 마지막으로 pull한 시간이 보인다.
만약에 의도한 시간이랑 다를시 docker push한것을 pull 했는지 확인해야된다.
CD workflow에서 run하기 전에 pull하도록 작성
echo "Pulling the latest Producer image"
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/roomie-producer:latest
echo "Pulling the latest Consumer image"
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/roomie-consumer:latest
찜하기 API에서 나온 race condition(동시성 상태 불일치)
찜하기 Patch api를 만들고, 그 api 호출을 이용해서 찜하기/해제를 구현했다.
동시에 다량의 찜하기/해제 api를 받을시, user_id, house_id가 같은 pin 엔티티가 2개이상 생기면서 서버 오류가 생기기 시작했다.
원인
찜하기/해제를 하나의 api를 이용해서 만들어서 race condition이 일어난 것이다.
pin은 house_id, user_id 필드를 이용해서 pin 여부를 기록하고 해제시 해당 엔티티를 삭제하는 방식이다.
Optional<Pin> pin = pinRepository.findByUserAndHouse(user, house);
동시에 찜하기
찜1 → DB read → 찜 엔티티가 없는것을 확인하고 찜하기 → DB 새로운 찜 객체 write
찜2 → DB read (새로운 찜 객체가 생성되기 전에 읽는다) → 찜엔티티 생성 전이므로 찜 없다고 확인하고 찜하기 → DB 새로운 찜 객체 write
찜1,2가 동시 다발적으로 일어나는데, read부터 write까지 atomicity하게 동작하지 않으므로 race condition이 발생했다.
즉 동일한 user와 house에 대해서 2개 이상의 찜 객체가 생성이 되게 된다.
다음 찜에서 DB read할때, 위 함수를 실행하면 IncorrectResultSizeDataAccessException이 발생한다.
해결
찜하기 api와 찜해제 api를 분리시키는것이 가장 이상적인 해결책이지만, 도중에 api바꾸기엔 클라이언트 입장에서 뒤늦은 결정이었다 .
그러므로 찜해제할때 동일한 찜객체 전부 삭제하고, 찜api 호출시 아래 함수로 찜여부를 검사하는것으로 수정했다.
List<Pin> findByUserIdAndHouseId(Long userId, Long houseId);
List empty()함수를 이용해서 찜 여부를 판단 할 수 있다.
찜 해제시 여부 판단하고 삭제시 모든 찜 객체를 탐색후 삭제하는 방식으로 해결했다.
batch는 엔티티에 리스트를 받아 일괄적으로 한번에 삭제하는 효율적인 jpa 함수이다.
if (!existingPins.isEmpty()) {
pinRepository.deleteAllInBatch(existingPins); // 모든 엔티티 삭제
멀티-서버 프로젝트에서 gradle 설정
프로젝트 세팅을 common, producer, consumer로 설정했다
common에서의 파일을 의존을 받아야 되기 때문에 아래와 같이 설정해야했다.
root 폴더가 Roomie이다.
Roomie/settings.gradle
rootProject.name = 'Roomie'
include 'common'
include 'producer'
include 'consumer'
producer/build.gradle, consumer/build.gradle
공통 모듈 추가 부분을 의존성에 선언해야지 build가 된다.
dependencies {
// 의존성들
implementation project(':common') // 공통 모듈 추가
}
application {
mainClassName = 'server.producer.ProducerApplication'
}
tasks.test {
useJUnitPlatform()
}
common 프로젝트 의존에서 순환참조 문제
producer나 consumer에서 common 모듈 사용하는 의존성을 추가하나
common/build.gradle에는 producer나 consumer에 대한 의존성 추가하면 순환 참조가 생긴다.
그러나 우리 프로젝트에서는 그 문제가 아닌 bootJar 순환 참조 문제가 생겼다.
common 모듈의 bootJar를 비활성화하는 것을 이용해서 의존성을 간소화 하여 해결했다.
common 모듈의 bootJar를 비활성화하고, 일반 JAR 파일을 생성하도록 설정했다.
이렇게 하면 common 모듈이 Spring Boot 실행 파일 형태로 패키징되지 않고, 라이브러리 형태로 활용되므로 의존성 관리가 간소화 된다.
common/build.gradle
jar {
enabled = true // JAR 파일 생성
}
bootJar {
enabled = false // bootJar 작업 비활성화
}
common 프로젝트에 등록한 엔티티의 의존을 받지 못하는 문제
common 프로젝트에다가 공통으로 쓰는 엔티티, dto를 등록을 하고 사용했고, IDE에서는 오류가 생기지 않으나 build하면 찾을수 없다는 오류가 생겼다
원인
Spring Boot는 기본적으로 애플리케이션 클래스의 위치를 기준으로 JPA 엔티티를 스캔한다. @SpringBootApplication은 아래의 세 가지 어노테이션을 포함한다:
-
@Configuration
-
@EnableAutoConfiguration
-
@ComponentScan (기본적으로 애플리케이션 클래스 패키지와 그 하위 패키지를 스캔)
이로 인해 Spring Boot는 @SpringBootApplication이 정의된 클래스의 패키지와 하위 패키지에서만 JPA 엔티티를 자동으로 검색합니다.
즉, common 패키지에 있는 엔티티는 producer의 하위 패키지가 아니므로 컴포넌트 스캔을 하지 않으므로 오류가 생긴 것이다.
해결
@EntityScan을 사용하여 common 패키지 를 명시적으로 지정했다.**
@EntityScan은 JPA 엔티티를 검색할 패키지를 명시적으로 설정할 수 있도록 한다.
즉, @EntityScan(basePackages = "entity")를 작성함으로써 Spring Boot가 entity 패키지에 위치한 모든 JPA 엔티티를 스캔하도록 지시할 수 있다.
@EntityScan(basePackages = "entity")
@SpringBootApplication
public class ProducerApplication {
public static void main(String[] args) {
SpringApplication.run(ProducerApplication.class, args);
}
}
@Scheduled 어노테이션을 이용했는데 스케쥴링이 안되는 문제
redis에서 2초마다 @Scheduled 어노테이션을 이용해서 2초마다 큐에서 메세지를 뽑아서 구현을 했다.
@Component
@RequiredArgsConstructor
@Slf4j
public class TourQueueScheduler {
private final TourQueueProcessor tourQueueProcessor;
@Scheduled(fixedDelay = 2000)
public void consumeMessages() {
String queueName = "tourRequest";
log.info("Consuming messages from queue: {}", queueName);
tourQueueProcessor.processMessage(queueName);
}
}
그러나 2초마다 해당 함수가 실행이 돼야 하나, 실행이 아에 되지 않았다.
원인
@EnableScheduling 미등록
@EnableScheduling을 반드시 붙여야 하는 이유는 Spring의 Scheduling 기능을 활성화하기 위해서이다. Spring은 기본적으로 Scheduling 기능을 자동으로 활성화하지 않으며, @EnableScheduling을 통해 해당 기능을 명시적으로 켜야 스케줄링 작업이 수행된다.
실수하기 쉬운 원인2
@Scheduled 메서드가 public이 아닌 경우
@Scheduled이 적용된 메서드는 public 접근 제어자를 가져야 한다. 접근 제어자가 protected 또는 private인 경우, 메서드가 실행되지 않는다.
해결
main class에 해당 어노테이션 추가
@EntityScan(basePackages = "entity")
@SpringBootApplication
@EnableScheduling
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
Repository test 작성시 @DataJpaTest , @Transactional 사용하게 된 과정
Repository Test시 h2 DB를 이용해서 테스트하고 싶지만 다른 설정과 확실하게 분리하고 싶어서 @SpringBootTest 이 아닌 @DataJpaTest를 사용했습니다.
@DataJpaTest
- 목적: JPA 관련 컴포넌트만 로드하고, 데이터베이스와 관련된 기능을 테스트하기 위한 설정입니다.
- 설명:
@DataJpaTest는 Spring Data JPA의 리포지토리 관련 기능만을 활성화하여 테스트를 수행하도록 합니다. 이를 통해 실제 데이터베이스에 의존하지 않고, JPA와 관련된 기능(예: 엔티티 매핑, 리포지토리 동작)을 빠르게 테스트할 수 있습니다. @Transactional과 결합되어 테스트 후 데이터베이스 상태가 롤백되므로 테스트 데이터가 DB에 영향을 미치지 않습니다.
@Transactional
- 목적: 테스트가 끝난 후 데이터베이스의 변경 사항을 롤백하여 테스트 데이터가 데이터베이스에 영향을 미치지 않도록 합니다.
- 설명:
@Transactional은 해당 테스트 메서드에서 발생한 모든 DB 트랜잭션을 하나의 트랜잭션으로 묶고, 테스트가 끝난 후 트랜잭션을 롤백하여 DB 상태를 원래대로 되돌립니다. 이렇게 하면 테스트 데이터가 DB에 영향을 미치지 않도록 할 수 있습니다. @DataJpaTest는 기본적으로 @Transactional을 활성화하지만, 명시적으로 추가하는 경우 명확하게 롤백을 보장할 수 있습니다.
Test시 의존받는 bean를 로드하지 못하는 문제
원인
@ContextConfiguration(classes = ProducerApplication.class)
- 목적: Spring의 ApplicationContext를 설정할 때 특정 클래스들을 로드하도록 지정합니다.
- 설명: 기본적으로
@DataJpaTest는 JPA 관련 설정만 로드하지만, ProducerApplication을 명시하여 ProducerApplication 클래스에서 정의한 빈들도 로드되게 합니다. 이는 필요할 경우 애플리케이션의 특정 설정이나 컴포넌트를 테스트에 포함시킬 수 있게 합니다.
@Import({FilterRepository.class, LocationLabeler.class})
• 목적: 테스트 클래스에서 사용할 특정 빈을 명시적으로 불러옵니다.
• 설명: @Import는 테스트 클래스에서 사용하고자 하는 다른 빈들을 불러올 수 있게 해줍니다. 이 예시에서는 FilterRepository와 LocationLabeler를 테스트에 포함시키기 위해 이 어노테이션을 사용하고 있습니다. @Import를 사용하면, 테스트 컨텍스트에 필요한 클래스를 명시적으로 추가할 수 있어 의존성을 주입하거나 사용할 수 있게 됩니다.
@ContextConfiguration은 ProducerApplication 클래스에서 정의된 빈과 설정들을 전반적으로 로드하지만, 테스트에서 사용해야 하는 모든 빈을 자동으로 로드하지는 않습니다.
@Import는 애플리케이션의 기본 설정 외에 특정 빈들만 따로 추가할 때 사용됩니다. FilterRepository와 LocationLabeler는 기본 설정에서 자동으로 로드되지 않거나 테스트에서 명시적으로 필요한 빈들이기 때문에 @Import를 통해 테스트 환경에 추가됩니다.
해결
@DataJpaTest
@ContextConfiguration(classes = ProducerApplication.class)
@EntityScan(basePackages = "entity")
@Import({FilterRepository.class, LocationLabeler.class})
@Transactional
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class FilterRepositoryTest {
@Autowired
private EntityManager entityManager;
@Autowired
private FilterRepository filterRepository;
@Autowired
private LocationLabeler locationLabeler;
user테이블과 같이 sql의 예약어랑 이름이 겹칠때 생기는 문제
원인
SQL 예약어는 데이터베이스에서 특별한 의미를 갖는 단어이므로, 이를 테이블 이름이나 컬럼 이름으로 사용할 경우 SQL 쿼리에서 충돌이 발생할 수 있다. 이로 인해 Hibernate가 자동으로 생성한 SQL 쿼리에서 예약어가 잘못 처리되어 쿼리가 실패할 수 있다.
해결
spring.jpa.properties.hibernate.globally_quoted_identifiers: true 설정을 사용하면 Hibernate가 테이블 및 컬럼 이름을 자동으로 큰따옴표(")로 감싸게 되어 예약어와 충돌을 방지할 수 있다. 예를 들어, user라는 테이블 이름을 사용하더라도 "user"로 감싸져 쿼리에서 예약어로 인식되지 않기 때문에 문제가 해결된다.
spring:
jpa:
properties:
hibernate:
globally_quoted_identifiers: true
test에서 @Autowired 어노테이션을 이용시 인스턴스의 상태공유가 안되는 문제
원인
JUnit 5의 기본 동작은 각 테스트 메서드 실행 시 테스트 클래스의 새로운 인스턴스를 생성하는 것입니다. 이로 인해 클래스 필드에 선언된 상태가 테스트 메서드 간에 공유되지 않고 초기화됩니다. 테스트 메서드가 같은 객체의 상태를 공유해야 하는 경우, 이러한 동작이 문제를 일으킬 수 있습니다. 특히, @Autowired로 주입된 객체가 클래스의 상태에 의존하는 경우, 각 테스트 메서드에서 별도의 인스턴스가 생성되어 기대한 동작이 이루어지지 않을 수 있습니다.
해결
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
- 목적: 테스트 클래스 인스턴스를 모든 테스트 메서드에서 하나만 생성하도록 설정합니다.
- 설명: 기본적으로 JUnit 5는 각 테스트 메서드 실행 시마다 테스트 클래스 인스턴스를 새로 생성합니다. 그러나 PER_CLASS를 사용하면 클래스 인스턴스를 한 번만 생성하고, 각 테스트 메서드는 그 인스턴스를 공유하게 됩니다. 이 설정을 사용하면, 클래스 필드에 선언된 상태를 테스트 간에 공유할 수 있어 성능을 개선할 수 있습니다. 예를 들어,
@BeforeEach에서 초기화된 값들이 테스트 간에 지속될 수 있습니다.
@DataJpaTest
@ContextConfiguration(classes = ProducerApplication.class)
@EntityScan(basePackages = "entity")
@Import({FilterRepository.class, LocationLabeler.class})
@Transactional
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class FilterRepositoryTest {
@Autowired
private EntityManager entityManager;
@Autowired
private FilterRepository filterRepository;
@Autowired
private LocationLabeler locationLabeler;
pin삭제시 연관된 user, house도 casecade되어 삭제되는 문제
원인
@ManyToOne 관계에 설정된 cascade = CascadeType.ALL은 부모 엔티티에서 발생하는 모든 영속성(Persistence) 작업(예: PERSIST, MERGE, REMOVE 등)을 연관된 자식 엔티티에도 전파하도록 지정합니다.
house와 user는 @ManyToOne 관계에서 Pin과 연관되어 있으며, CascadeType.ALL로 설정되어 있습니다. 따라서 Pin 삭제 시 JPA는 이 연관된 엔티티들도 함께 삭제하도록 동작합니다.
pinRepository.deleteByUserAndHouse(user, house); // 여기서 삭제 됨
@Entity
@Table(name="pin")
@Data
public class Pin {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "house_id")
private House house;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "user_id")
private User user;
}
CascadeType은 JPA에서 연관 관계를 맺고 있는 엔티티들 간에 특정 작업을 어떻게 전파할지 설정하는 옵션이다. 주로 부모 엔티티에서 수행된 작업이 자식 엔티티에 전파되도록 하기 위해 사용된다.
CascadeType 종류
- PERSIST: 부모 엔티티가 저장될 때, 연관된 자식 엔티티도 저장된다. 예를 들어, CascadeType.PERSIST를 설정하면 부모 엔티티를 저장할 때 자식 엔티티도 자동으로 저장된다.
- MERGE: 부모 엔티티가 병합될 때, 연관된 자식 엔티티도 병합된다. 부모 엔티티가 병합될 때 자식 엔티티의 상태도 반영된다.
- REMOVE: 부모 엔티티가 삭제될 때, 연관된 자식 엔티티도 삭제된다. 부모 엔티티를 삭제할 때 연관된 자식 엔티티도 함께 삭제된다.
- REFRESH: 부모 엔티티가 리프레시될 때, 연관된 자식 엔티티도 리프레시된다. 부모 엔티티의 상태가 DB와 일치하도록 갱신될 때 자식 엔티티도 갱신된다.
- DETACH: 부모 엔티티가 분리될 때, 연관된 자식 엔티티도 분리된다. 부모 엔티티가 영속성 컨텍스트에서 분리되면 자식 엔티티도 함께 분리된다.
- ALL: 모든 작업이 자식 엔티티에 전파된다. 즉, PERSIST, MERGE, REMOVE, REFRESH, DETACH 모두 자식 엔티티에 전파된다.
해결
CascadeType.PERSIST 로 설정하여 저장될때만 연관되고, 삭제될땐 pin말고 다른 엔티티도 전파되지 않도록 설정합니다.
@Entity
@Table(name="pin")
@Data
public class Pin {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinColumn(name = "house_id")
private House house;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinColumn(name = "user_id")
private User user;
}
지연 로딩하려 할 때 세션/em이 닫혀 LazyInitializationException이 발생하는 문제
원인
지연 로딩(Lazy Loading) 관련 오류
FetchType.LAZY는 엔티티 관계에서 연관된 다른 엔티티를 지연 로딩 방식으로 로딩하겠다는 의미이다.
그러나 지연 로딩을 사용하는 엔티티가 프록시 객체로 반환되기 때문에, 실제로 해당 엔티티가 사용될 때까지 로딩되지 않는다.
이 때문에, 관계된 엔티티가 사용될 때 LazyInitializationException이 발생할 수 있다. 특히, @ManyToOne 관계가 설정된 엔티티가 트랜잭션 밖에서 접근될 때 발생한다.
해결 방법
-
트랜잭션 내에서 사용: LAZY 로딩을 사용할 때, 해당 엔티티를 사용하는 곳에서 트랜잭션이 유지되도록 하여, LazyInitializationException이 발생하지 않도록 해야 한다. 예를 들어, 서비스 레벨에서 트랜잭션을 관리하고, 그 안에서 관련 데이터를 접근할 수 있게 한다.
즉, 하나의 트랜잭션 함수 안에서 필요한 작업들을 전부 해야됩니다.
-
FetchType.EAGER 로 설정: 연관된 데이터를 항상 불러오게 되나, 그러면 항상 불러올때 조인해서 불러온다는 성능상 단점이 있습니다.
즉, 해당 객체 자체만 불러오고 싶으면 조인 하지 않도록 별도의 커스텀 jpa 함수를 작성해야되므로 비효율적이라고 느꼈습니다.
-
커스텀 jpa함수 작성: 조인이 필요할때만 필요한 만큼 조인하고 그 외에는 jpa 기본 함수 쓰는게 성능상 제일 안전하다고 생각했습니다.
해결
커스텀 jpa함수를 정의해서 조인해서 받아 오도록 했습니다.
@Query("SELECT h FROM House h " +
"LEFT JOIN FETCH h.rooms r " +
"WHERE h.id = :houseId")
Optional<House> findHouseWithRoomsById(@Param("houseId") Long houseId);
DTO를 Record로 변환하고 하나의 파일로 통합한 과정
원인
기존 코드에서는 세 가지 DTO 클래스(HouseInfoDto, RoomDto, RoommateDto)가 각각 별도의 파일로 분리되어 있었는데 이 구조로 인해 다음과 같은 문제점이 발생했다.
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HouseInfoDto {
private Long houseId;
private String name;
private String mainImgUrl;
private String monthlyRent;
private String deposit;
private String location;
private String occupancyTypes;
private String occupancyStatus;
private String genderPolicy;
private int contractTerm;
private List<String> moodTags;
private String roomMood;
private List<String> groundRule;
private int maintenanceCost;
private boolean isPinned;
private List<String> safetyLivingFacility;
private List<String> kitchenFacility;
}
@Data
@Builder
public class RoomDto {
private Long roomId;
private String name;
private boolean status;
private int occupancyType;
private String gender;
private int deposit;
private int prepaidUtilities;
private int monthlyRent;
private String contractPeriod;
private String managementFee;
}
@Data
@Builder
public class RoommateDto {
private String name;
private int age;
private String job;
private String mbti;
private String sleepTime;
private String activityTime;
}
- DTO 파일 관리 복잡성:
- 세 개의 DTO 클래스가 각각 다른 파일에 존재하여 관리가 불편했음. 관련된 데이터 모델임에도 파일이 분리되어 있어 연관성을 파악하기 어려웠음.
- 수정이나 확장 시 여러 파일을 열어야 했고, 코드의 전반적인 가독성이 떨어짐.
- 중복된 Boilerplate(별 수정 없이 반복적으로 사용되는 코드) 코드:
- DTO 클래스마다
@Data, @Builder, @NoArgsConstructor, @AllArgsConstructor를 반복적으로 선언해야 했음.
- 이는 코드가 불필요하게 길어지고 유지보수의 부담을 가중시킴.
- 일관된 데이터 구조 제공 부족:
- 특정 엔드포인트에서
HouseInfoDto, RoomDto, RoommateDto를 조합하여 응답을 반환할 때, 이 세 클래스가 독립적으로 관리되다 보니 직관적인 구조를 제공하기 어려웠음.
- 이로 인해 API 응답 객체를 파악하기 힘들고, 가독성이 떨어지는 JSON 응답 구조가 만들어질 수 있었음.
- 코드 유지보수 및 확장성 저하:
- 연관된 클래스들이 분리되어 있어 변경사항이 발생하면 각각의 파일을 수정해야 했고, 이로 인해 유지보수성이 저하됨.
- 특히 연관성이 높은 데이터 구조를 한곳에서 확인하고 관리할 수 있는 장점이 사라졌음.
해결
이러한 문제를 해결하기 위해 다음과 같은 방식을 적용하였다.
public record HouseDetailsResponseDto (
HouseInfoDto houseInfo,
List<RoomDto> rooms,
List<RoommateDto> roommates
) {
@Builder
public HouseDetailsResponseDto{
}
public record HouseInfoDto (
Long houseId,
String name,
String mainImgUrl,
String monthlyRent,
String deposit,
String location,
String occupancyTypes,
String occupancyStatus,
String genderPolicy,
int contractTerm,
List<String> moodTags,
String roomMood,
List<String> groundRule,
int maintenanceCost,
boolean isPinned,
List<String> safetyLivingFacility,
List<String> kitchenFacility
) {
@Builder
public HouseInfoDto{
}
}
public record RoomDto (
Long roomId,
String name,
boolean status,
int occupancyType,
String gender,
int deposit,
int prepaidUtilities,
int monthlyRent,
String contractPeriod,
String managementFee
) {
@Builder
public RoomDto {
}
}
public record RoommateDto (
String name,
int age,
String job,
String mbti,
String sleepTime,
String activityTime
) {
@Builder
public RoommateDto {
}
}
}
Record로 전환:
- Java의
record는 간결한 문법을 제공하여 데이터 모델을 작성할 때 반복적인 boilerplate 코드를 제거해줌.
- 기존 DTO에서 사용되던
@Data, @Builder, @NoArgsConstructor, @AllArgsConstructor를 제거하고, record 문법으로 대체함으로써 클래스 정의를 단순화함.
- 단일 응답 객체로 통합:
HouseDetailsResponseDto를 생성하여 HouseInfoDto, RoomDto, RoommateDto를 하나의 응답 객체로 묶음.
- 응답 데이터의 구조를 명확히 하고 관련 데이터 간의 관계를 직관적으로 보여줄 수 있도록 설계함.
- 내부
record 정의로 그룹화:
HouseDetailsResponseDto 내부에 HouseInfoDto, RoomDto, RoommateDto를 정의하여 관련된 데이터 구조를 한 파일에서 관리할 수 있도록 함.
- 이를 통해 데이터 간의 연관성을 명확히 하고, 유지보수성을 향상시켰음.
이와 같은 변경을 통해 한 파일에서 관련된 데이터를 관리할 수 있게 되어, 전체적인 코드 구조가 단순화되고 가독성이 개선되었고, 연관된 데이터를 한곳에서 관리함으로써 새로운 필드 추가나 수정이 간편해졌다.
Swap memory를 이용해서 메모리 부족 해결
Swap 메모리는 RAM이 부족할 때 HDD의 일부를 RAM처럼 사용하는 방식으로, 시스템 안정성을 높이는 데 유용하다. AWS 권장 기준에 따르면, RAM이 2GB 이하일 경우 Swap 공간은 RAM 용량의 2배를 설정하는 것이 좋다. 예를 들어, RAM이 1GB인 t2.micro 환경에서는 Swap 메모리를 통해 2GB를 추가하여 총 3GB 메모리로 확장 가능하다.
Swap 메모리 생성 및 활성화
-
Swap 파일 생성
sudo dd if=/dev/zero of=/swapfile bs=128M count=16
- of: 파일 경로
- bs: Block 사이즈
- count: Block 개수→ 위 설정은 128MB * 16 = 2GB Swap 메모리를 생성.
-
권한 설정 및 활성화
sudo chmod 600 /swapfile # 읽기/쓰기 권한 업데이트
sudo mkswap /swapfile # Swap 영역 설정
sudo swapon /swapfile # Swap 활성화
sudo swapon -s # 활성화 확인
-
부팅 시 자동 활성화 설정
# 파일 열기
sudo vi /etc/fstab
/swapfile swap swap defaults 0 0 # 파일 마지막에 추가
주의 사항
- Swap 메모리는 HDD를 기반으로 하므로, 속도가 RAM보다 약 10만배(이론상 지연시간) 느리다. (보통은 100~1000배)
- Swap을 활용하더라도 시스템 성능 저하를 고려하고 적절히 관리해야 한다.
Tip: free 명령어로 Swap 메모리가 정상적으로 추가되었는지 확인할 수 있다.
🚨 트러블 슈팅 목록 (~1/24)
MultipleBagFetchException발생 문제@Scheduled(fixedDelay=)어노테이션을 이용했는데 스케쥴링이 안되는 문제@DataJpaTest,@Transactional사용하게 된 과정@Autowired어노테이션을 이용시 인스턴스의 상태공유가 안되는 문제Record로 변환하고 하나의 파일로 통합한 과정다중 LEFT JOIN 시
MultipleBagFetchException발생 문제문제
테스트 중, 다음과 같은 쿼리에서
MultipleBagFetchException이 발생함:MultipleBagFetchException은 Hibernate에서 여러 개의@OneToMany관계를 fetch join할 경우 발생하는 문제로, 한 번의 쿼리로 여러 개의 컬렉션(List)을 동시에 로드하려고 시도하면 발생함.House에는rooms,pins,recentlyViewedHouses세 가지@OneToMany관계가 있었는데, 위의 쿼리는rooms,pins와rooms내의roommates를 모두 fetch join하여 한 번의 쿼리로 데이터를 가져오려고 시도하였기 때문에 오류가 발생하였다.Hibernate는 SQL에서 결과를 하나의 Cartesian Product로 생성하고, 이를 Java 컬렉션으로 매핑하는 방식으로 작동한다. 하지만 여러 개의 컬렉션을 동시에 fetch join할 경우, 이 결과를 적절히 분해하지 못해 문제가 발생하는 것이다.
@OneToMany컬렉션을 fetch join하면, 결과 집합이 중복되거나 불필요한 Cartesian Product가 생성됨. Hibernate는 이를 적절히 처리하지 못해MultipleBagFetchException을 던짐.해결
모든 관계를 한 번에 fetch join하지 않도록 쿼리를 분리
rooms,roommates,pins데이터를 한 쿼리에서 동시에 가져오지 않고, 필요한 관계만 분리된 쿼리에서 fetch join하도록 수정함.다음과 같이 쿼리를 나누어 정의:
✋🏻 여기서 잠깐
Fetch Join / Left Join 설명
1. Fetch Join
Fetch Join은 Hibernate와 같은 ORM에서 엔티티와 연관된 컬렉션이나 엔티티를 즉시 로딩(Eager Loading) 방식으로 가져오기 위해 사용하는 기법이다.JOIN과는 다르게, 엔티티 그래프(Entity Graph)를 로드하는 데 사용된다.JOIN FETCH키워드를 사용하여 명시적으로 설정할 수 있다.MultipleBagFetchException이 발생할 수 있다.2. Left Join
Left Join은 SQL에서 사용하는 조인 방식으로, 기준 테이블의 모든 데이터와 조인된 테이블의 일치하는 데이터를 결합하여 결과를 반환한다.NULL값이 들어간다.NULL값을 포함한 데이터가 반환될 수 있어, 클라이언트에서 이를 처리해야 한다.Fetch Join과 Left Join의 차이
Docker hub을 이용해서 CD환경 구축했을때, 적용한 최신 내용이 적용 안되는 문제
원인
CD를 위해 docker hub으로 images를 push는 하고 있었으나, pull하고 docker를 실행시키지 않았다.
아래 명령어로는 기존에 이미지가 없을때는 이미지를 pull 하나, 기존에 이미지가 있을땐 pull을 하지 않는 문제다.
해결
docker images 명령어를 사용하면 보유 이미지와 마지막으로 pull한 시간이 보인다.
만약에 의도한 시간이랑 다를시 docker push한것을 pull 했는지 확인해야된다.
CD workflow에서 run하기 전에 pull하도록 작성
찜하기 API에서 나온 race condition(동시성 상태 불일치)
찜하기 Patch api를 만들고, 그 api 호출을 이용해서 찜하기/해제를 구현했다.
동시에 다량의 찜하기/해제 api를 받을시, user_id, house_id가 같은 pin 엔티티가 2개이상 생기면서 서버 오류가 생기기 시작했다.
원인
찜하기/해제를 하나의 api를 이용해서 만들어서 race condition이 일어난 것이다.
pin은 house_id, user_id 필드를 이용해서 pin 여부를 기록하고 해제시 해당 엔티티를 삭제하는 방식이다.
동시에 찜하기
찜1 → DB read → 찜 엔티티가 없는것을 확인하고 찜하기 → DB 새로운 찜 객체 write
찜2 → DB read (새로운 찜 객체가 생성되기 전에 읽는다) → 찜엔티티 생성 전이므로 찜 없다고 확인하고 찜하기 → DB 새로운 찜 객체 write
찜1,2가 동시 다발적으로 일어나는데, read부터 write까지 atomicity하게 동작하지 않으므로 race condition이 발생했다.
즉 동일한 user와 house에 대해서 2개 이상의 찜 객체가 생성이 되게 된다.
다음 찜에서 DB read할때, 위 함수를 실행하면 IncorrectResultSizeDataAccessException이 발생한다.
해결
찜하기 api와 찜해제 api를 분리시키는것이 가장 이상적인 해결책이지만, 도중에 api바꾸기엔 클라이언트 입장에서 뒤늦은 결정이었다 .
그러므로 찜해제할때 동일한 찜객체 전부 삭제하고, 찜api 호출시 아래 함수로 찜여부를 검사하는것으로 수정했다.
List empty()함수를 이용해서 찜 여부를 판단 할 수 있다.
찜 해제시 여부 판단하고 삭제시 모든 찜 객체를 탐색후 삭제하는 방식으로 해결했다.
batch는 엔티티에 리스트를 받아 일괄적으로 한번에 삭제하는 효율적인 jpa 함수이다.
멀티-서버 프로젝트에서 gradle 설정
프로젝트 세팅을 common, producer, consumer로 설정했다
common에서의 파일을 의존을 받아야 되기 때문에 아래와 같이 설정해야했다.
root 폴더가 Roomie이다.
Roomie/settings.gradle
producer/build.gradle, consumer/build.gradle
공통 모듈 추가 부분을 의존성에 선언해야지 build가 된다.
common 프로젝트 의존에서 순환참조 문제
producer나 consumer에서 common 모듈 사용하는 의존성을 추가하나
common/build.gradle에는 producer나 consumer에 대한 의존성 추가하면 순환 참조가 생긴다.
그러나 우리 프로젝트에서는 그 문제가 아닌 bootJar 순환 참조 문제가 생겼다.
common 모듈의 bootJar를 비활성화하는 것을 이용해서 의존성을 간소화 하여 해결했다.
common 모듈의 bootJar를 비활성화하고, 일반 JAR 파일을 생성하도록 설정했다.
이렇게 하면 common 모듈이 Spring Boot 실행 파일 형태로 패키징되지 않고, 라이브러리 형태로 활용되므로 의존성 관리가 간소화 된다.
common/build.gradle
common 프로젝트에 등록한 엔티티의 의존을 받지 못하는 문제
common 프로젝트에다가 공통으로 쓰는 엔티티, dto를 등록을 하고 사용했고, IDE에서는 오류가 생기지 않으나 build하면 찾을수 없다는 오류가 생겼다
원인
Spring Boot는 기본적으로 애플리케이션 클래스의 위치를 기준으로 JPA 엔티티를 스캔한다.
@SpringBootApplication은 아래의 세 가지 어노테이션을 포함한다:@Configuration@EnableAutoConfiguration@ComponentScan(기본적으로 애플리케이션 클래스 패키지와 그 하위 패키지를 스캔)이로 인해 Spring Boot는
@SpringBootApplication이 정의된 클래스의 패키지와 하위 패키지에서만 JPA 엔티티를 자동으로 검색합니다.즉, common 패키지에 있는 엔티티는 producer의 하위 패키지가 아니므로 컴포넌트 스캔을 하지 않으므로 오류가 생긴 것이다.
해결
@EntityScan을 사용하여 common 패키지 를 명시적으로 지정했다.**@EntityScan은 JPA 엔티티를 검색할 패키지를 명시적으로 설정할 수 있도록 한다.즉,
@EntityScan(basePackages = "entity")를 작성함으로써 Spring Boot가 entity 패키지에 위치한 모든 JPA 엔티티를 스캔하도록 지시할 수 있다.@Scheduled어노테이션을 이용했는데 스케쥴링이 안되는 문제redis에서 2초마다
@Scheduled어노테이션을 이용해서 2초마다 큐에서 메세지를 뽑아서 구현을 했다.그러나 2초마다 해당 함수가 실행이 돼야 하나, 실행이 아에 되지 않았다.
원인
@EnableScheduling미등록@EnableScheduling을 반드시 붙여야 하는 이유는 Spring의 Scheduling 기능을 활성화하기 위해서이다. Spring은 기본적으로 Scheduling 기능을 자동으로 활성화하지 않으며,@EnableScheduling을 통해 해당 기능을 명시적으로 켜야 스케줄링 작업이 수행된다.실수하기 쉬운 원인2
@Scheduled메서드가 public이 아닌 경우@Scheduled이 적용된 메서드는 public 접근 제어자를 가져야 한다. 접근 제어자가 protected 또는 private인 경우, 메서드가 실행되지 않는다.해결
main class에 해당 어노테이션 추가
Repository test 작성시
@DataJpaTest,@Transactional사용하게 된 과정Repository Test시 h2 DB를 이용해서 테스트하고 싶지만 다른 설정과 확실하게 분리하고 싶어서
@SpringBootTest이 아닌@DataJpaTest를 사용했습니다.@DataJpaTest@DataJpaTest는 Spring Data JPA의 리포지토리 관련 기능만을 활성화하여 테스트를 수행하도록 합니다. 이를 통해 실제 데이터베이스에 의존하지 않고, JPA와 관련된 기능(예: 엔티티 매핑, 리포지토리 동작)을 빠르게 테스트할 수 있습니다.@Transactional과 결합되어 테스트 후 데이터베이스 상태가 롤백되므로 테스트 데이터가 DB에 영향을 미치지 않습니다.@Transactional@Transactional은 해당 테스트 메서드에서 발생한 모든 DB 트랜잭션을 하나의 트랜잭션으로 묶고, 테스트가 끝난 후 트랜잭션을 롤백하여 DB 상태를 원래대로 되돌립니다. 이렇게 하면 테스트 데이터가 DB에 영향을 미치지 않도록 할 수 있습니다.@DataJpaTest는 기본적으로@Transactional을 활성화하지만, 명시적으로 추가하는 경우 명확하게 롤백을 보장할 수 있습니다.Test시 의존받는 bean를 로드하지 못하는 문제
원인
@ContextConfiguration(classes = ProducerApplication.class)@DataJpaTest는 JPA 관련 설정만 로드하지만, ProducerApplication을 명시하여 ProducerApplication 클래스에서 정의한 빈들도 로드되게 합니다. 이는 필요할 경우 애플리케이션의 특정 설정이나 컴포넌트를 테스트에 포함시킬 수 있게 합니다.@Import({FilterRepository.class, LocationLabeler.class})• 목적: 테스트 클래스에서 사용할 특정 빈을 명시적으로 불러옵니다.
• 설명:
@Import는 테스트 클래스에서 사용하고자 하는 다른 빈들을 불러올 수 있게 해줍니다. 이 예시에서는 FilterRepository와 LocationLabeler를 테스트에 포함시키기 위해 이 어노테이션을 사용하고 있습니다.@Import를 사용하면, 테스트 컨텍스트에 필요한 클래스를 명시적으로 추가할 수 있어 의존성을 주입하거나 사용할 수 있게 됩니다.@ContextConfiguration은 ProducerApplication 클래스에서 정의된 빈과 설정들을 전반적으로 로드하지만, 테스트에서 사용해야 하는 모든 빈을 자동으로 로드하지는 않습니다.@Import는 애플리케이션의 기본 설정 외에 특정 빈들만 따로 추가할 때 사용됩니다. FilterRepository와 LocationLabeler는 기본 설정에서 자동으로 로드되지 않거나 테스트에서 명시적으로 필요한 빈들이기 때문에@Import를 통해 테스트 환경에 추가됩니다.해결
user테이블과 같이 sql의 예약어랑 이름이 겹칠때 생기는 문제
원인
SQL 예약어는 데이터베이스에서 특별한 의미를 갖는 단어이므로, 이를 테이블 이름이나 컬럼 이름으로 사용할 경우 SQL 쿼리에서 충돌이 발생할 수 있다. 이로 인해 Hibernate가 자동으로 생성한 SQL 쿼리에서 예약어가 잘못 처리되어 쿼리가 실패할 수 있다.
해결
spring.jpa.properties.hibernate.globally_quoted_identifiers: true 설정을 사용하면 Hibernate가 테이블 및 컬럼 이름을 자동으로 큰따옴표(")로 감싸게 되어 예약어와 충돌을 방지할 수 있다. 예를 들어, user라는 테이블 이름을 사용하더라도 "user"로 감싸져 쿼리에서 예약어로 인식되지 않기 때문에 문제가 해결된다.
test에서
@Autowired어노테이션을 이용시 인스턴스의 상태공유가 안되는 문제원인
JUnit 5의 기본 동작은 각 테스트 메서드 실행 시 테스트 클래스의 새로운 인스턴스를 생성하는 것입니다. 이로 인해 클래스 필드에 선언된 상태가 테스트 메서드 간에 공유되지 않고 초기화됩니다. 테스트 메서드가 같은 객체의 상태를 공유해야 하는 경우, 이러한 동작이 문제를 일으킬 수 있습니다. 특히,
@Autowired로 주입된 객체가 클래스의 상태에 의존하는 경우, 각 테스트 메서드에서 별도의 인스턴스가 생성되어 기대한 동작이 이루어지지 않을 수 있습니다.해결
@TestInstance(TestInstance.Lifecycle.PER_CLASS)@BeforeEach에서 초기화된 값들이 테스트 간에 지속될 수 있습니다.pin삭제시 연관된 user, house도 casecade되어 삭제되는 문제
원인
@ManyToOne관계에 설정된 cascade = CascadeType.ALL은 부모 엔티티에서 발생하는 모든 영속성(Persistence) 작업(예: PERSIST, MERGE, REMOVE 등)을 연관된 자식 엔티티에도 전파하도록 지정합니다.house와 user는
@ManyToOne관계에서 Pin과 연관되어 있으며, CascadeType.ALL로 설정되어 있습니다. 따라서 Pin 삭제 시 JPA는 이 연관된 엔티티들도 함께 삭제하도록 동작합니다.CascadeType은 JPA에서 연관 관계를 맺고 있는 엔티티들 간에 특정 작업을 어떻게 전파할지 설정하는 옵션이다. 주로 부모 엔티티에서 수행된 작업이 자식 엔티티에 전파되도록 하기 위해 사용된다.
CascadeType 종류
해결
CascadeType.PERSIST 로 설정하여 저장될때만 연관되고, 삭제될땐 pin말고 다른 엔티티도 전파되지 않도록 설정합니다.
지연 로딩하려 할 때 세션/em이 닫혀 LazyInitializationException이 발생하는 문제
원인
지연 로딩(Lazy Loading) 관련 오류
FetchType.LAZY는 엔티티 관계에서 연관된 다른 엔티티를 지연 로딩 방식으로 로딩하겠다는 의미이다.
그러나 지연 로딩을 사용하는 엔티티가 프록시 객체로 반환되기 때문에, 실제로 해당 엔티티가 사용될 때까지 로딩되지 않는다.
이 때문에, 관계된 엔티티가 사용될 때 LazyInitializationException이 발생할 수 있다. 특히,
@ManyToOne관계가 설정된 엔티티가 트랜잭션 밖에서 접근될 때 발생한다.해결 방법
트랜잭션 내에서 사용: LAZY 로딩을 사용할 때, 해당 엔티티를 사용하는 곳에서 트랜잭션이 유지되도록 하여, LazyInitializationException이 발생하지 않도록 해야 한다. 예를 들어, 서비스 레벨에서 트랜잭션을 관리하고, 그 안에서 관련 데이터를 접근할 수 있게 한다.
즉, 하나의 트랜잭션 함수 안에서 필요한 작업들을 전부 해야됩니다.
FetchType.EAGER 로 설정: 연관된 데이터를 항상 불러오게 되나, 그러면 항상 불러올때 조인해서 불러온다는 성능상 단점이 있습니다.
즉, 해당 객체 자체만 불러오고 싶으면 조인 하지 않도록 별도의 커스텀 jpa 함수를 작성해야되므로 비효율적이라고 느꼈습니다.
커스텀 jpa함수 작성: 조인이 필요할때만 필요한 만큼 조인하고 그 외에는 jpa 기본 함수 쓰는게 성능상 제일 안전하다고 생각했습니다.
해결
커스텀 jpa함수를 정의해서 조인해서 받아 오도록 했습니다.
DTO를
Record로 변환하고 하나의 파일로 통합한 과정원인
기존 코드에서는 세 가지 DTO 클래스(
HouseInfoDto,RoomDto,RoommateDto)가 각각 별도의 파일로 분리되어 있었는데 이 구조로 인해 다음과 같은 문제점이 발생했다.@Data,@Builder,@NoArgsConstructor,@AllArgsConstructor를 반복적으로 선언해야 했음.HouseInfoDto,RoomDto,RoommateDto를 조합하여 응답을 반환할 때, 이 세 클래스가 독립적으로 관리되다 보니 직관적인 구조를 제공하기 어려웠음.해결
이러한 문제를 해결하기 위해 다음과 같은 방식을 적용하였다.
Record로 전환:record는 간결한 문법을 제공하여 데이터 모델을 작성할 때 반복적인 boilerplate 코드를 제거해줌.@Data,@Builder,@NoArgsConstructor,@AllArgsConstructor를 제거하고,record문법으로 대체함으로써 클래스 정의를 단순화함.HouseDetailsResponseDto를 생성하여HouseInfoDto,RoomDto,RoommateDto를 하나의 응답 객체로 묶음.record정의로 그룹화:HouseDetailsResponseDto내부에HouseInfoDto,RoomDto,RoommateDto를 정의하여 관련된 데이터 구조를 한 파일에서 관리할 수 있도록 함.이와 같은 변경을 통해 한 파일에서 관련된 데이터를 관리할 수 있게 되어, 전체적인 코드 구조가 단순화되고 가독성이 개선되었고, 연관된 데이터를 한곳에서 관리함으로써 새로운 필드 추가나 수정이 간편해졌다.
Swap memory를 이용해서 메모리 부족 해결
Swap 메모리는 RAM이 부족할 때 HDD의 일부를 RAM처럼 사용하는 방식으로, 시스템 안정성을 높이는 데 유용하다. AWS 권장 기준에 따르면, RAM이 2GB 이하일 경우 Swap 공간은 RAM 용량의 2배를 설정하는 것이 좋다. 예를 들어, RAM이 1GB인 t2.micro 환경에서는 Swap 메모리를 통해 2GB를 추가하여 총 3GB 메모리로 확장 가능하다.
Swap 메모리 생성 및 활성화
Swap 파일 생성
권한 설정 및 활성화
부팅 시 자동 활성화 설정
주의 사항
Tip:
free명령어로 Swap 메모리가 정상적으로 추가되었는지 확인할 수 있다.