부트캠프/TIL

[부트캠프] TIL - Optional에 대하여 + @Transactional

purple95 2024. 8. 26. 20:06

Jpa를 사용한 개인 과제를 진행하며 리턴 타입을 Optional로 받으며 마주할 수 밖에 없는 부분에 대해서

학습하며 진행하고자 남깁니다.

 

public ScheduleResponseDTO getSchedule(Long id) {
    // Schedule 검색      검색 이전에 id를 가진 Schedule이 존재하는지 여부 확인 작업 추가 가능    ** 단일 검색기능은 update에서도 공통으로 쓰이므로 함수로 따로 빼도될것.
    // .get()의 경우 결과값이 null일 경우 NoSuchElementException 발생
    Schedule schedule = scheduleRepository.findById(id).get();

    // Entity -> ResponseDTO
    return new ScheduleResponseDTO(schedule);
}

 

Id(Primary Key)값을 기반으로 Schedule 데이터 하나를 가져오는 ScheduleService class의 메소드입니다.

 

이 부분에서 JpaRepository를 상속받는 scheduleReposiotry의 메소드를 사용하여 값을 가져오는데

.get() 메소드를 사용하는 경우가 있습니다.

 

.get()메소드를 사용함으로써 결과 값이 존재하지 않는 경우에 대한 처리를 예측가능한 수준으로 처리할 수 있습니다.

 

.get()의 결과가 null인 경우 NoSuchElementException 발생.

존재하지않는 Id값을 파라미터로 넘겨준 Test 코드 실행 결과

 

항상 하는 어떤 DB의 검색, 수정, 삭제 작업 시에는 찾고자 하는 요소가 DB에 존재하는지의 유무를 사전에 검사하는

작업이 들어가게 됩니다, 그 부분에서 JpaRepository를 사용한 메소드의 리턴값에 .get()을 사용하면, 값이 존재하지

않는 경우에 대한 처리가 좁혀지므로 try~ catch를 사용하여 처리해줄 수 있겠습니다.

 

.orElseThrow()로 값이 없을 경우 예외를 발생

따로 지정한 예외클래스를 전달할 수도 있고, 직접 예외메시지를 입력할 수도 있습니다.

// throw Exception
return sampleRepository.findById(id)
                       .orElseThrow(IllegalArgumentException::new);
    
return sampleRepository.findById(id)
                       .orElseThrow(() -> new IllegalArgumentException("no such data");

 

.orElse() 또는 .orElseGet()을 사용하여 값이 없는 경우 값을 지정

// null Return
return sampleRepository.findById(id)
                       .orElse(null);
    
// 비어있는 객체로 Return
return sampleRepository.findById(id)
                       .orElseGet(Sample::new);

존재하지 않는 Id값을 Get할 경우 -> orElse(null) 처리했을 때 NullPointerException이 발생하는걸 볼 수 있다.

 

 


@Transactional 중요내용

Test코드를 작성하다보면 @Transactional을 사용하는데 이 부분에 있어서 중요한 내용이 있다.

@Test
@Transactional
@Rollback(value = false)
public void createSchedule(){
    Schedule schedule = new Schedule();
    schedule.setUsername("sungrae");
    schedule.setTitle("스케쥴제목");
    schedule.setContents("스케쥴 내용");

    //Schedule 반환.
    //findBy~~~ 식으로 사용하는 경우 Optional타입을 반환한다고 하는 부분 찾아보기.
    Schedule result = scheduleRepository.save(schedule);

    System.out.println(result.getUsername());
    System.out.println(result.getSchedule_id());
}

 

진행중인 과제의 테스트 코드의 일부입니다. 해당 부분에서는 우선 @Transactional이 없어도 상관없습니다.

이유는 Repository.save()에 @Transactional이 이미 포함되어있는 상태입니다.

 

	@Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null");
        if (this.entityInformation.isNew(entity)) {
            this.entityManager.persist(entity);
            return entity;
        } else {
            return this.entityManager.merge(entity);
        }
    }

SimpleJpaRepository의 save()메소드입니다, 해당 메소드에 자체적으로 @Transactional 어노테이션이 적용되어 있기 때문에 save()하나만 사용한 테스트코드의 경우 @Transactional 어노테이션이 붙을 필요는없습니다, 다만

다음과 같은 경우에는 반드시 붙어야만 합니다.

 

@Test
@Transactional
void test(){
	Food food = new Food();
    food.setName("test");
    foodRepository.save(food);
    
    Food food2 = new Food();
    food.setPrice(1000);
    foodRepository.save(food2);
}

//Food 클래스는 name 멤버변수에 @Column(nullable=false) 가 달렸습니다.

해당 코드의 경우 두번째 food2 를 save()하는 경우에 에러가 발생합니다, 이때 문제가 없던 앞부분의 save(food)는

쿼리가 정상적으로 날아갔지만, Rollback이 됩니다, 그 이유는 @Transactional로 테스트메소드가 묶여있으며

두번째의 save(food2)에서 에러가 발생하면서 전체가 Rollback이 되기때문입니다.

그렇다면 아래와 같은 상황을 비교해보겠습니다.

 

@Test
void test(){
	Food food = new Food();
    food.setName("test");
    foodRepository.save(food);
    
    Food food2 = new Food();
    food.setPrice(1000);
    foodRepository.save(food2);
}

//Food 클래스는 name 멤버변수에 @Column(nullable=false) 가 달렸습니다.

 

@Transactional이 빠진 상태입니다, 하지만 JpaRepository를 상속한 foodRepository.save()자체에 @Transactional이

걸려있기때문에 food.setName("test"); 뒤에 save(food)를 하고 바로 쿼리를 날려보냅니다.

테스트 메소드에 @Transactional이 없기때문입니다, 그러고 food2를 설정하고 save(food2)를 하며 에러가 발생합니다.

 

여기에서 두 상황의 차이는 두번째 테스트 메소드는 첫 번째의 save()메소드가 DB에 반영이되었다는겁니다.

 

따라서, @Transactional을 걸어주는 상황은 필요한 상황에 따라서 달라진다는 점이 중요합니다, 메소드 내부에

save() 메소드가 여러개가 존재해도 만약, 이중에 하나라도 문제가 발생할 경우 모든 실행을 취소해야 한다면

아무리 save()에 @Transactional이 자체적으로 걸려있다고 할지라도, 여러개의 save()를 포함한 메소드 자체에 

@Transactional을 걸어줘야 하겠고, 여러 save()중 일부가 실패하더라도 상관없다면 메소드에 @Transactional을 걸지 않아도 되겠습니다.