전체 글

728x90
JPA에서 단방향 및 양방향 일대일 관계의 외래키 처리와 지연 로딩 문제

JPA에서 단방향 및 양방향 일대일 관계의 외래키 처리와 지연 로딩 문제

❗️일대일 대상 테이블에 외래키 단방향 관계는 JPA에서 지원하지 않으며, 양방향 관계만 지원합니다.

 

[인프런 김영한님 자료]

memberAndLockerImage

대상 테이블에 외래키가 존재하는 경우, 프록시 기능의 한계로 인해 지연 로딩으로 설정해도 항상 즉시 로딩됩니다.

예를 들어, Member 객체를 가져왔을 때 JPA는 가져온 Member 객체에 Locker가 있는지 여부를 알아야 합니다.

 

1. 외래키가 Locker에 있을 때

외래키가 Locker에 있다면, Member 엔티티를 가져올 때 Member 클래스에 정의된 Locker가 존재하는지 확인해야 합니다. 이때 Member 테이블에는 외래키가 존재하지 않기 때문에, 지연 로딩 설정 여부와 상관없이 Locker 테이블을 조회하여 Member와 연결된 Locker가 있는지 확인하는 추가 쿼리가 발생합니다.

 

2. 외래키가 Member에 있을 때

외래키가 Member에 있다면, Member 테이블의 조회만으로도 Member와 연결된 Locker가 있는지 여부를 판단할 수 있습니다. 따라서 이 경우에는 지연 로딩(lazy loading) 또는 즉시 로딩(eager loading) 설정이 가능합니다.

 

요약

  • 외래키가 Locker에 있는 경우: Member를 조회할 때 항상 Locker 테이블을 추가로 조회해야 하므로, 지연 로딩 설정이 무시되고 즉시 로딩이 이루어집니다.
  • 외래키가 Member에 있는 경우: Member 테이블만으로 Locker와의 연결 여부를 확인할 수 있으므로, 지연 로딩과 즉시 로딩 설정이 가능합니다.
728x90
728x90
@DynamicInsert

[Spring] @DynamicInsert 사용이유

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Knowledge {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	@Column(nullable = false)
	private String thumbnailImageUrl;
	@Column(nullable = false)
	private String contentImageUrl;
	@Column(columnDefinition = "boolean default false")
	private Boolean isDeleted;
	@Column(updatable = false)
	@CreationTimestamp
	private LocalDateTime createdAt;

}

위와 같은 knowledge 객체를 생성 및 저장 할 때 isDeleted 필드 값을 설정하지 않으면, 데이터베이스에 null 값이 저장됩니다. 이는 데이터베이스에 기본값이 true 또는 false로 설정되어 있더라도 발생할 수 있는 문제입니다.

 

예를 들어, Knowledge Entity 가 위와 같을때 Knowledge 객체를 저장하게 되면(isDeleted 가 null 인 상태로), 아래와 같은 쿼리가 생성됩니다:

2024-01-03T16:21:44.759+09:00 DEBUG 32029 --- [           main] org.hibernate.SQL                        : 
    insert 
    into
        knowledge
        (thumbnail_image_url, contentImageUrl, isDeleted, createdAt) 
    values
        (?, ?, ?, ?)

이 쿼리는 isDeleted 필드가 명시적으로 null로 설정되었기 때문에, 데이터베이스에 false가 아닌 null이 저장되는 문제를 발생시킵니다.

 

이를 해결하기 위해 @DynamicInsert 어노테이션을 사용할 수 있습니다. @DynamicInsert를 사용하면 isDeleted 필드가 null일 경우 INSERT 문에서 제외됩니다. 따라서 데이터베이스는 isDeleted 필드에 대한 기본값을 적용하여 false를 저장하게 됩니다.

2024-01-03T16:21:44.759+09:00 DEBUG 32029 --- [           main] org.hibernate.SQL                        : 
    insert 
    into
        knowledge
        (thumbnail_image_url, content_image_url, created_at) 
    values
        (?, ?, ?)

@DynamicInsert 어노테이션을 사용하면, 엔티티 객체의 null 값을 가진 필드를 INSERT 문에서 제외하여 데이터베이스의 기본값이 적용되도록 할 수 있습니다. 이를 통해 Knowledge 엔티티 클래스의 isDeleted 필드가 올바르게 처리되며, 불필요한 null 값 저장을 방지할 수 있습니다.

728x90
728x90
optional 클래스의 orElseThrow

Optional 클래스의 orElseThrow

orElseThrowOptional 클래스의 메서드로, 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 예외를 던지도록 설계되었습니다. 이 메서드는 Supplier 인터페이스를 통해 예외를 생성하는데, Supplier는 인자를 받지 않고 결과를 반환하는 함수형 인터페이스입니다.

 

orElseThrow 메서드는 다음과 같이 정의되어 있습니다

 public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
      if (value != null) {
          return value;
      } else {
          throw exceptionSupplier.get();
      }
  }

이 메서드는 값이 존재하면 그 값을 반환하고, 값이 없으면 Supplier를 통해 예외를 생성하여 던집니다. Supplier@FunctionalInterface 어노테이션이 붙은 인터페이스로, 단 하나의 추상 메서드 get을 가지고 있습니다

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

`

orElseThrow 메서드를 사용할 때는 보통 람다 표현식을 사용하여 예외 객체를 생성합니다. 예를 들어, find 메서드에서 특정 ID에 해당하는 Knowledge 객체를 찾지 못했을 때 예외를 던지도록 다음과 같이 작성할 수 있습니다:

public Knowledge find(Long knowledgeId) {
		return knowledgeRepository.findById(knowledgeId).orElseThrow(() -> new CustomRuntimeException(
			KnowledgeException.KNOWLEDGE_NOT_FOUND));
	}

이 코드는 knowledgeId에 해당하는 Knowledge 객체가 존재하지 않으면 CustomRuntimeException을 던집니다. 여기서 사용된 람다 표현식 () -> new CustomRuntimeException(KnowledgeException.KNOWLEDGE_NOT_FOUND)Supplier<CustomRuntimeException> 타입의 인스턴스를 생성합니다. (CustomRuntimeException 구조)

 

최종적으로 위와같이

public Knowledge find(Long knowledgeId) {
		return knowledgeRepository.findById(knowledgeId).orElseThrow(() -> new CustomRuntimeException(
			KnowledgeException.KNOWLEDGE_NOT_FOUND));
	}

예외가 던져진다하면 orElseThrow 안에 제네릭은 타입 추론에 의하여

public <CustomRuntimeException extends Throwable> T orElseThrow(Supplier<? extends CustomRuntimeException> exceptionSupplier) throws CustomRuntimeException {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}

이렇게 바뀌고 value 즉 위에선 Knowledge 가 없는경우 CustomRuntimeExceptionthrow 되게 됩니다.

그후 ControllerAdvice 등의 예외 처리 로직에 의하여 예외가 처리 됩니다.

728x90
728x90
Enum 타입으로 Exception 구현하기

[Spring] Enum 타입으로 Exception 구현하기

기존에 예외 처리를 다음과 같이 Enum 타입으로 구현했습니다.

public enum AdminException {

	FAIL_TO_SIGN_IN(HttpStatus.BAD_REQUEST, "로그인에 실패했습니다.");

	private final HttpStatus status;
	private final String message;
}

 

Enum 타입은 기본적으로 java.lang.Enum을 암시적으로 상속받기 때문에 extends 키워드를 사용할 수 없습니다. 따라서 상속 기능을 구현하기 위해 interface를 사용했으며, interface는 다중 상속을 지원하므로 아래 코드와 같이 enum 클래스들이 CustomException 인터페이스를 implements 하도록 했습니다.

public interface CustomException {
	
	HttpStatus getHttpStatus();

	String getErrorMessage();

	String getName();
}

@RequiredArgsConstructor
public enum AdminException implements CustomException {

	FAIL_TO_SIGN_IN(HttpStatus.BAD_REQUEST, "로그인에 실패했습니다.");

	private final HttpStatus status;
	private final String message;

	@Override
	public HttpStatus getHttpStatus() {
		return status;
	}

	@Override
	public String getErrorMessage() {
		return message;
	}

	@Override
	public String getName() {
		return name();
	}
}

 

Enum 타입이 특정 인터페이스를 implements한 이유는 RuntimeException을 상속받는 특정 클래스(아래에서는 CustomRumtimeException)에서 모든 예외에 대해 동일한 응답을 제공하기 위해서였습니다. 해당 클래스는 다음과 같습니다.

@Getter
@RequiredArgsConstructor
public class CustomRuntimeException extends RuntimeException {

	private final CustomException customException;

	public String getMessage() {
		return customException.getErrorMessage();
	}
}

위 코드는 CustomRuntimeException 클래스가 CustomException 인터페이스를 구현하는 열거형을 사용하여 예외 정보를 처리하고, 예외 발생 시 일관된 응답을 반환하도록 설계되었습니다. 이를 통해 각 예외에 대한 HTTP 상태 코드와 메시지를 통일된 형식으로 관리할 수 있습니다.

최종적으로 CustomRuntimeExceptionControllerAdvice 또는 RestControllerAdvice에서 아래와 같이 예외를 잡고 응답을 보낼 수 있습니다.

@ExceptionHandler(CustomRuntimeException.class)
public ResponseEntity<Map<String, String>> customExceptionHandler(CustomRuntimeException e) {

    // 응답 맵 생성
    Map<String, String> responseMap = new LinkedHashMap<>();
    responseMap.put("status", e.getCustomException().getHttpStatus().toString());
    responseMap.put("message", e.getCustomException().getErrorMessage());

    // 응답 반환
    return ResponseEntity.status(e.getCustomException().getHttpStatus())
        .body(responseMap);
}

이렇게 하면 예외 발생 시 CustomRuntimeException을 던지고, GlobalExceptionHandler가 해당 예외를 잡아 일관된 형식으로 응답을 반환하게 됩니다.

728x90

'Spring' 카테고리의 다른 글

[Spring] DynamicInsert 사용 이유  (0) 2024.07.22
Optional 클래스의 orElseThrow  (0) 2024.07.22
docker hub 사용하는 방법  (1) 2023.11.27
redis를 통해 로그아웃 기능 구현  (0) 2023.11.27
Jasypt 를 통한 암호화  (2) 2023.11.27
728x90
ssl tls 인증하기

SSL/TLS 인증 추가하기

image-20231130002009365

https 를 사용하는 ElasticSearch 를 사용해 데이터를 조회하려 했는데

unable to find valid certification path to requested target 와 같은 오류가 발생했다.

해당오류는 SSL 인증서가 없기 때문에 발생한 오류다. 따라서 SSL 인증서를 추가해야하며 방법은 아래와 같다.


1. cacert 에 import 하기

기본적으로 SSL 인증서를 등록하기 위해선 당연히 SSL 인증서가 필요하다.

docker 를 통해 ElasticSearch 를 실행했다면, 아래와 같은 명령어로 인증서를 복사할수 있다.

docker cp [ElasticContainerID]:/usr/share/elasticsearch/config/certs/http_ca.crt .

위 명령어를 통해 http_ca.crt 인증서를 복사했다면 해당 인증서를 JAVAkeystore 에 등록해줘야한다.

 

keystore 에 등록하는 방법은 cacerts 라는 파일에 import 를 해주면 된다.

[sudo] keytool -import -alias [별칭이름] -keystore [cacerts 경로] -file [http_ca.crt 경로]

나같은 경우에 cacerts 의 경로는 아래와 같았다.

/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/lib/security/cacerts

참고로 권한이 없다고하면 sudo 를 붙여 명령어를 실행해주자.

cacertsimport 를 하기 위해선 비밀번호가 필요한데 바꾸지 않았다면 changeit 이다.

 

cacerts 파일에 ssl 인증서를 import 했다면, 이젠 intellij 에서 vm 옵션을 통해 아래 명령어를 추가해주면 된다.


2. Intellij 의 VM 옵션 추가하기

-Djavax.net.ssl.trustStore=[cacerts 경로] -Djavax.net.ssl.trustStorePassword=[cacerts 비밀번호]

이제 다시 실행해보면 https 를 사용하는 elasticSearch 로부터 정상적인 응답을 받는것을 확인할수 있다.

image-20231130003108446

 

728x90

'Java' 카테고리의 다른 글

[Java] OOP 4대원칙 및 SOLID원칙  (0) 2023.04.09

docker hub 사용하는 방법

2023. 11. 27. 18:31
728x90
docker 이미지 push and pull

1. docker 홈페이지 가서 아이디 만들기

image-20230802111748922

내 프로필은 charliekorean 이다.

 

2. local 에서 docker에 로그인하기

docker login

위 명령어를 통해 docker에 login 할수있다. docker의 아이디 및 비밀번호를 입력하면 된다.

 

3. docker hub에 image push 하기

image-20230802111931822

현재 docker에 존재하는 모든 이미지중 mysql:latest 를 테스트삼아 docker hub에 올려보겠다.

 

우리가 docker hub에 이미지를 올리게되면 repository가 생성이 되어 그안에 우리가 push한 이미지가 존재하게 된다. 이때 repository명은 항상 고유해야한다. 따라서 {username}/{repository}:{tag} 형식으로 위 이미지 명을 바꿔줘야한다.

 

docker tag mysql:latest charliekorean/mysql:latest

image-20230802112312843

위 명령어를 입력후 image를 출력해보면 같은 image ID에 대한 서로다른 repository 명을 가진 image가 생성된것을 알수있다.

 

이후 이 이미지를 docker hub로 push하면 된다.

docker push charliekorean/mysql:latest

 

4. docker hub에 올라간 이미지 확인

 

image-20230802112511591

 

5. docker hub에 올린 이미지 pull 받기

docker pull charliekorean/mysql:latest

간단하다 우리가 올린 image를 그대로 pull 해주고 image를 확인해보면 잘 다운받아 진것을 알수있다.

728x90

+ Recent posts