728x90

이펙티브 자바를 읽으면서 내 기술을 확장하고 싶었다. 그래서 부족한 부분은 채우고 앞으로의 개발에 적용해보려고 한다.

1. 객체의 생성과 파괴

생성자 대신 정적 팩토리 메서드를 고려하라

클래스는 클라이언트에 public 생성자 대신 정적 팩토리 메서드를 제공할 수 있다.

정적 팩토리 메소드가 생성자보다 좋은 장점

  • 이름을 가질 수 있다.
  • 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
  • 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
  • 정적 펙토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

단점

  • 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다.
  • 정적 팩토리 메서드는 프로그래머가 찾기 어렵다.

정적 팩토리 메서드의 명명 방식

  • from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
    • Date d = Date.from(instant);
  • of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
    • Set<Rank> faceCards = EnumSet.of(JACK, KING, QUEEN);
  • valueOf: from과 of의 자세한 버전
  • instacne 혹은 getInstance: (매개변수를 받는다면)매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
  • create 혹은 newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
  • getType: getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 사용한다. Type은 팩토리 메서드가 반환할 객체의 타입이다.
  • newType: newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 사용한다. Type은 팩토리 메서드가 반환할 객체의 타입이다.
  • type: getType과 newType의 간결한 버전

핵심정리

정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇지만 정적 팩토리를 사용하는게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하는 습관은 고치자😊

생성자에 매개변수가 많다면 빌더를 고려해라

생성자를 오버로딩하여 만든 점층적 생성자 패턴도 사용은 가능하지만, 매개변수가 엄청 많아지게 된다면 클라이언트 코드를 작성하거나 읽기가 어렵다.
이럴 때 활용하는 대안이 바로 자바빈즈 패턴이다.
기본 생성자로 객체를 초기화하고, setter 메소드를 사용하여 매개변수 값을 설정해주는 방식이다.

public class Apple {
    private int price;
    private String taste;

    public Apple() {
        //기본값이 이것을 생성
    }

    private getPrice() {
        return price;
    }

    private getTaste() {
        return taste;
    }

    private setPrice(int price) {
        this.price = price;
    }

    private setTaste(String taste) {
        this.taste = taste;
    }
}

class Main {
    public static void main(String[] args) {
        Apple apple = new Apple();
        apple.setPrice(1000);
        apple.setTaste("맛있다");
    }
}

이런식으로 구성된 것이 바로 자바빈즈 패턴이다.
그런데 이것도 단점인 이유는 객체 하나를 만드려면 지금은 인자가 2개라서 별로 못느낄 수 있겠지만, 메서드를 여러개 호출해야하고, 완전히 생성된 것이 아니라면 일관성이 무너진 상태에 놓인다.

점층적 생성자 패턴에서는 매개변수들이 유효한지는 생성자에서 확인하면 되었지만, 이 패턴은 그런 것이 없다. 그래서 이 패턴에서는 클래스를 불변으로 만들 수 없다.

빌더 패턴 - 점층적 생성자, 자바빈즈 패턴의 장점만 취함

필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(혹은 정적 팩토리)를 호출해 빌더 객체를 얻는다. 그 다음 세터 메서드로 원하는 선택적 매개변수 세팅을 한다. 그 후 build 메서드를 호출하여 객체를 생성한다.

여기에 null값을 체크하는 유효성 검사를 넣어주면 완벽한 객체 생성기가 될것이다.

핵심정리

생성자나 정적 팩토리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다.

Private 생성자나 열거 타입으로 싱글톤임을 보증하라

싱글톤(Singleton)은 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.
클래스를 싱글톤으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.

대부분 상황에서는 원소가 하나뿐인 Enum(열거) 타입이 싱글톤을 만드는 가장 좋은 방법이다.

❗ 단, 만드려는 싱글톤이 Enum 외의 클래스를 상속해야 한다면 사용할 수 없다.

인스턴스화를 막으려면 private 생성자 사용

추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다. 왜냐면 하위 클래스를 만들어 인스턴스화를 진행할 수 있기 때문이다. 이것으로 인해 사용하는 개발자가 상속해서 쓰라는 뜻으로 오해할 수 있는 우려가 있다.

컴파일러가 기본 생성자를 만드는 경우는 명시된 생성자가 없을 때밖에 없으니 private생성자를 추가하면 클래스의 인스턴스화를 방지할 수 있다.

728x90

'Java' 카테고리의 다른 글

TDD Clean Code with Java 12기 2주차  (0) 2022.08.05
[JPA] 객체 지향 쿼리 심화  (0) 2022.08.05
MockMvc  (0) 2022.08.04
Mock, Mockito  (0) 2022.08.04
728x90

넥스트스텝에서 주최한 TDD, 클린코드 with Java 12기를 신청하게 되었다.

개인적으로 테스트 주도로 개발하는 것을 너무 지향했고 혼자 공부하면서 지식을 습득했었는데 이런 좋은 강의를 통해서 기존에 스터디원들과도 같이 성장할 수 있는 계기가 또 한가지가 생기게 되었다.😄

넥스트스텝은 개발자가 소프트웨어 장인으로 성장하는데에 필요한 모든 도움을 주는 것이 비전이자 목표라고 한다.

자동차 경주의 후기는 단위 테스트 코드 작성에 대한 것보다 더 나아가서 TDD를 하려고 무조건 조금씩이라도 테스트를 해가면서 기능을 구현하는 것을 습관화 하면 저절로 뒤로 클린코드가 따라오는 것 같다.

아직 익숙하지 않고 길들여지진 않았지만 다음 미션들을 차근차근 해나가면 TDD에 적응될 것 같다!!

각 코드리뷰 PR이다.
학습테스트 실습
문자열계산기
자동차 경주
자동차 경주(우승자)
자동차 경주(리팩토링)

점점 객체지향 설계에 대한 필요성을 느끼고 있다.
이펙티브 자바도 같이 읽으면서 이 강의에 대해 조금 더 힘을 싣는다면 좋은 결과가 나올 것같다.

728x90

'Diary' 카테고리의 다른 글

블로그를 옮기고 최신 근황  (0) 2022.08.13
업무 리팩토링에 대한 회고  (0) 2022.08.10
업무 회고  (0) 2022.08.07
1개월 1일 1커밋 회고  (0) 2022.08.07
728x90

JPA의 데이터 타입을 크게 분류하면 엔티티 타입과 값 타입으로 나눈다.
엔티티 타입은 @Entity로 정의하는 객체이고, 값 타입은 int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다.
값 타입은 3가지로 나눌 수 있다.

  • 기본 값 타입
    • 자바 기본타입
    • 래퍼(Wrapper) 클래스
    • String
  • 임베디드 타입(복합 값)
  • 컬렉션 값 타입

기본 값 타입은 말그대로 자바가 제공하는 기본 데이터 타입이고, 임베디드 타입은 JPA에서 사용자가 직접 정의한 값이다. 컬렉션 타입은 하나 이상의 값 타입을 저장할 때 사용한다.

기본은 다뤘으므로 생략하도록 하겠다.

임베디드 타입(복합 값 타입)

직접 정의한 임베디드 타입도 int, String 처럼 값 타입이다.

공통적으로 쓰는 어떤것(ex - 시간, 주소)들을 엔티티 클래스 마다 그대로 가지고 있으면 객체지향적이지 않으며 응집력이 떨어진다. 이런 공통 타입이 생기면 더 명확해진다.

임베디드 타입을 사용하려면 2가지 어노테이션이 필요하다.
둘 중 하나는 생략해도 된다.

  • @Embeddable : 값 타입을 정의하는 곳에 표시
  • @Embedded : 값 타입을 사용하는 곳에 표시

임베디드 타입은 엔티티의 값일 뿐이다. 따라서 값이 속한 엔티티의 테이블에 매핑한다.
이 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는것이 가능하기 때문에 잘 설계된 애플리케이션은 매핑한 테이블 수보다 클래스의 수가 더 많다.

Mybatis로 개발을 한다면 테이블, 객체 1:1매핑을 한다. 그렇기에 객체지향으로 개발하려고 해도 이미 SQL에 너무나 의존적인 개발을 진행했기에 여러 클래스를 매핑하는 작업이 수월하지 않았다.

ORM인 JPA를 사용하면 귀찮은 반복 작업은 JPA에게 할당하고 모델을 설계하는데 집중할 수 있다.

이 기능으로 연관된 테이블은 모조리 @Embedded로 묶어 사용하는 그림이 그려진다❗️

@AttributeOverride : 속성 재정의

만약 주소가 집주소 그리고 회사 주소가 있다고 가정할때 클래스는 똑같은데 컬럼 값을 다르게 줘야한다면 속성을 재정의해서도 값을 줄수가 있다.

@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class Address {
    @Column(name = "city")
    private String city;

    private String street;

    private String zipcode;
}

@Embedded
private Address homeAddress;

@Embedded
@AttributeOverrides({
    @AttributeOverride(name = "city", column = @Column(name = "COMPANY_CITY")),
    @AttributeOverride(name = "street", column = @Column(name = "COMPANY_STREET")),
    @AttributeOverride(name = "zipcode", column = @Column(name = "COMPANY_ZIPCODE")),
})
private Address companyAddress;

이렇게해서 재정의를 해주면 companyAddress에는 override한 @Column 들이 매핑되게 된다.
그래서 SQL 쿼리문은 아래와 같이 출력된다.

여기서 column이 소문자로 나온 이유는 내가 JPA Buddy로 column설정을 무조건 언더바에 lowerCase로 나오게해서 그렇다.

설정이 없다면 대문자로 나오게 될 것이다.

이런식으로 공통적으로 쓰는것은 저번 강의에서 봤던 @MappedSuperClass와 같이 사용한다면 시너지가 극대화 될 것이라고 생각한다❗️

임베디드 타입과 null

임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.

member.setAddress(null); //null
em.persist(member);

멤버 테이블의 주소와 관련된 값은 모두 null이 된다.

값 타입과 불변 객체

값 타입은 단순하고 안전하게 다룰 수 있어야 한다.

값 타입 공유 참조

임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity"); //멤버1의 address 공유
member2.setHomeAddress(address);

이렇게 update를 하게되면 멤버2만 NewCity로 변경이 되는 것이 아니라 멤버1의 주소도 NewCity로 변경된다. 이것은 영속성 컨텍스트가 멤버1, 2 둘 다 city 속성값이 변경된 것으로 생각하기 때문에 둘다 update 쿼리를 날리게 된다.

이런식으로 예상치 못한 곳에서 문제가 발생하는 것은 부작용이라고 한다. 이 부작용을 막기 위해선 값을 복사해서 사용하면 된다.

값 타입 복사

값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다. 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본타입이 아니라 객체 타입이다.
자바 객체는 CallByReference 이기 때문에 참조값을 전달한다.
clone()이 아니라 예를 들어

Address a = new Address("주소1");
Address b = a;
b.setCity("주소테스트");

이렇게 b Address에 a가 참조하는 인스턴스의 참조값 자체를 b에 넘기면 둘은 같은 인스턴스를 공유참조 한다. 이렇게되면 a의 city값도 변하게 되는 것이다.

인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있는데 복사하지 않고 원본 참조 값을 직접 넘기는 실수를 완전하게 막을 수는 없다. 그래서 객체의 공유 참조는 피할 수 없다.

책에서 해결책은 setter메소드를 모두 제거하면 된다고한다. 제거하면 부작용의 발생을 막을 수 있다.

불변 객체

객체를 불변하게 만들면 값을 수정할 수 없다. 그렇기에 부작용 원천 차단이 가능하다.
따라서 값 타입은 될 수 있으면 불변 객체로 설계해야 한다.
불변 객체의 값은 조회할 수 있지만 수정할 수 없다. 이 불변 객체도 객체기에 참조값 공유를 피할 수는 없지만 수정이 불가능하므로 부작용이 발생할 우려는 없다.

이런데에서 깨달은 것이 바로 생성자에서 값을 할당하는 것이다.

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Address {
    private String city;

    public Address(String city) {
        this.city = city;
    }
}

멤버1의 주소값을 조회하여 새로운 주소 생성

Address address = member1.getHomeAddress();
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);

이렇게 해서 불변이라는 작은 제약조건으로 부작용이라는 에러를 막을 수 있다.

값 타입의 비교

이것은 너무나 잘 알고들 있을거라고 생각한다.

  • 동일성 비교 : 인스턴스의 참조 값을 비교(주소 값을 비교하는것)
    • == 사용
  • 동등성 비교 : 인스턴스의 단순 값을 비교
    • equals() 사용

요약

엔티티 타입과 값 타입의 특징은 다음과 같다.

엔티티 타입의 특징

  • 식별자(@Id) 가 있다.
    • 엔티티 타입은 식별자가 있고 식별자로 구별할 수 있다.
  • 생명 주기가 있다.
    • 생성 → 영속화 → 소멸 의 주기가 있다.
    • em.persist(entity) 로 영속화
    • em.remove(entity) 로 제거
  • 공유할 수 있다.
    • 참조 값 공유할 수 있다. 이것이 공유 참조라고 한다.
    • ex : 멤버 엔티티가 있으면 다른 엔티티에서 얼마든지 멤버 엔티티 참조 가능

값 타입 특징

  • 식별자 없음
  • 생명 주기를 엔티티에 의존함
    • 스스로 생명주기를 가지지 않고 엔티티에 의존한다. 의존하는 엔티티를 제거하면 같이 제거된다.
  • 공유하지 않는 것이 안전하다.
    • 엔티티 타입과는 다르게 공유하지 않는 것이 안전하다. 대신 값을 복사하자 ‼️
    • 오직 하나의 주인만 관리해야 함.
    • 불변 객체로 만드는 것이 안전

엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만드는 실수는 범하지 말자!!!

728x90

'JPA' 카테고리의 다른 글

[JPA] 객체 지향 쿼리 언어 - 2  (0) 2022.08.05
[JPA] 객체지향 쿼리 언어 - 1  (0) 2022.08.05
[JPA] 프록시  (0) 2022.08.05
[JPA] 고급 매핑  (0) 2022.08.04
728x90

Docker란?

  • 컨테이너 기술을 지원하는 다양한 프로젝트중 하나
  • 컨테이너 기술의 사실상 표준
  • 다양한 운영체제에서 사용이 가능하다.(Linux, Mac OS, Windows)
  • 애플리케이션에 국한되지 않고 의존성 및 파일 시스템까지 패키징하여 빌드, 배포, 실행을 단순화
  • Linux의 NameSpace와 cgroups 커널 기능을 사용하여 가상화

도커는 다양한 클라우드 서비스 모델과 같이 사용가능

  • 이미지 : 필요한 프로그램과 라이브러리, 소스를 설치한 뒤 만든 하나의 파일
  • 컨테이너 : 이미지를 격리하여 독립된 공간에서 실행한 가상 환경

컨테이너가 해결한다!

  • 동일 시스템에서 실행하는 소프트웨어의 컴포넌트가 충돌하거나 다양한 종속성을 갖고 있음
  • 컨테이너는 가상머신을 사용해 각 마이크로 서비스를 격리하는 기술
  • 컨테이너는 가상머신처럼 하드웨어를 전부 구현하지 않기 때문에 매우 빠른 실행이 가능
  • 프로세스의 문제가 발생할 경우 컨테이너 전체를 조정해야 하기 때문에 컨테이너에 하나의 프로세스를 실행하도록 하는것이 좋다.

컨테이너, 서비스, 서버 가 많아질수록 비용이 절감되는 효과를 볼 수 있다❗

컨테이너를 격리하는 기술

리눅스 네임스페이스 : 각 프로세스가 파일 시스템 마운트, 네트워크, 유저, 호스트네임 등에 대해 시스템에 독립뷰 제공

리눅스 컨트롤 그룹 : 프로세스로 소비할 수 있는 리소스 양(CPU, 메모리, I/O, 네트워크 대역 등)을 제한

도커의 한계

서비스가 커지면 커질수록 관리해야 하는 컨테이너의 양이 급격히 증가
도커를 사용하여 관리를 한다고 하더라도 쉽지 않은 형태
배포 및 컨테이너 batch전략
scale-in, scale-out이 어려움

Docker는 OS의 자원을 이용하기 때문에 기본적으로 Root 사용자에서 명령어를 사용해야 한다.
Docker Docs 바로가기

설치

sudo -i # root 접속
# 비밀번호 있으면 입력 없으면 그대로
# 명령어 치는 곳 $에서 # 으로 변경 확인
apt install docker.io # 도커 설치

search(image 검색)

docker search !@%!@

  • Docker 허브로부터 사용가능한 이미지를 찾는 명령어
  • Docker는 Docker Hub를 통해 GitHub처럼 사용자들간의 이미지 공유를 할 수 있는 환경이 구축되어 있다.
  • 공식이미지는 / 앞에 사용자 이름이 붙지 않는것이다.

Pull(image 다운로드)

docker pull tomcat:latest
Docker 허브로부터 이미지 다운로드

images(image 목록 보기)

# docker images
현재 PC에 다운 받아져있는 image들을 출력하는 명령어

run

컨테이너 생성과 동시에 컨테이너로 접속하는 명령어

# docker run "REPOSITORY"
(docker run <옵션><이미지이름 or 이미지ID><실행할 파일>)

  • 단순히 image안의 파일을 실행할 목적으로 생성된 것이기 때문에 메인으로 실행되는 파일이 종료되면 컨테이너도 같이 종료된다. 따라서 계속해서 컨테이너를 유지하고 싶다면 -d 옵션을 이용해야 한다.

  • 옵션

    • -i : interactive
      • 사용자가 입출력을 할 수 있는 상태로 한다.
    • -t : 가상 터미널 환경을 에뮬레이션 하겠다는 설정
    • -d : 컨테이너를 일반 프로세스가 아닌 데몬프로세스 형태로 실행하여 프로세스가 끝나도 유지되도록 한다.

create

컨테이너 생성 명령어
# docker create <옵션> <포트번호> <이름> <이미지명>
docker create -d -p 80:80 --name nx nginx
이런식으로 명령어 사용이 가능하다
컨테이너 계층을 생성하고 명령을 실행하게끔만 만드는 단계이다.

옵션

  • -p : 컨테이너의 포트를 호스트에 게시함
  • --name : 컨테이너에 이름 할당

start

docker start [OPTIONS] CONTAINER [CONTAINER...]
create에서 할당한 이름값으로 시작할 수 있고 containerId로도 실행이 가능하다.

stop

docker stop id or container
실행중인 container를 중지시킨다.
-t 옵션으로 일정시간 지난후에 중지 시킬수도 있다.

ps

컨테이너 목록보기 명령어
docker ps로 볼수있다.
기본값은 실행되고있는 컨테이너만 표시한다.
그래서 중지된 컨테이너도 보려면 docker ps -a 를 사용하면 된다.

옵션

  • --all, -a
    • 모든 컨테이너 표시(기본값은 실행만 표시)
  • --quiet, -q
    • 컨테이너 ID만 표시

remove

제거 명령은 container를 중지 시킨 후에 가능하다

docker rm containerId or name

중지시킨후에 container 제거 명령을 할 수있다.

rmi는 이미지를 지우는 것으로 설치했던것을 삭제하는 명령이다.

docker rmi nginx 이런식으로 받았던 이미지 이름을 rmi 뒤에 넣어준다.

컨테이너 내부 shell 실행

sudo docker exec -it tc /bin/bash

컨테이너 로그 확인

sudo docker logs tc # stdout, stderr

728x90

'클라우드' 카테고리의 다른 글

OpenSSH  (0) 2022.08.09
Docker compose  (0) 2022.08.07
728x90

프록시와 즉시로딩, 지연로딩

객체는 객체 그래프로 연관된 객체들을 탐색.
그렇지만 객체가 DB에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기는 어렵다.
JPA구현체들은 이것을 해결하기 위해 프록시라는 기술을 사용한다. 프록시를 사용하면 연관된 객체를 처음부터 DB에서 조회하는 것이 아니라, 실제 사용하는 시점에 DB에서 조회할 수 있다.
자주 함께 사용하는 객체들은 조인을 사용해서 함께 조회 하는것이 효과적이다.
JPA는 즉시로딩과 지연로딩이라는 방법으로 둘을 모두 지원한다.

영속성 전이와 고아 객체

JPA는 연관된 객체를 함께 저장하거나 함께 삭제할 수 있는 영속성 전이와 고아 객체 제거라는 편리한 기능을 제공한다.

프록시

엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다. 멤버 엔티티를 조회할 때 연관된 팀 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만, 그렇지 않을 때도 있다.

@Entity @Getter
@NoArgsConstructor
public class Member implements Serializable {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private int age;

    @ManyToOne
    private Team team;

    @Builder
    public Member(Long id, String name, int age, Team team) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.team = team;
    }
}

@Entity
@Getter
public class Team {

    @Id
    @GeneratedValue
    private Long id;

    private String name;


    @Builder
    public Team(String name){
        this.name = name;
    }
}

아래는 테스트 코드이다.

@Test
@DisplayName("프록시 테스트")
void proxyTest(){
    //given
    tx.begin();

    Team team = Team.builder()
                    .name("team1")
                    .build();


    Member member = Member.builder()
                          .name("홍길동")
                          .age(27)
                          .team(team)
                          .build();

    em.persist(team);
    em.persist(member);
    em.flush();

    //when
    Member resultMember = em.find(Member.class, member.getId());
    Team resultTeam = resultMember.getTeam();

    //then
    assertThat(resultMember.getName()).isEqualTo("홍길동");
    assertThat(resultMember.getAge()).isEqualTo(27);
    assertThat(resultTeam.getName()).isEqualTo("team1");
}

이 코드는 멤버 아이디로 회원 엔티티를 찾으면서 연관된 팀의 이름도 출력한다.
그런데 만약 멤버 엔티티만 조회한다고 한다면 팀 엔티티까지 DB에서 조회해두는 것은 비효율적이다.

JPA는 이러한 문제를 해결하려고 엔티티가 실제 사용될 때까지 DB 조회를 지연하는 방법을 제공해준다.
이것을 지연로딩이라고 한다.
정리하자면 실제 팀 엔티티 값을 사용하는 시점에 DB에서 팀 엔티티에 필요한 데이터를 조회하는 것이다.

지연 로딩을 사용하기 위해선 실제 엔티티 객체 대신 DB조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라고 한다.

프록시 기초

JPA에서 식별자로 엔티티 하나를 조회할 때에는 EntityManager.find()를 사용한다.
이 메소드는 영속성 컨텍스트에 엔티티가 존재하지 않으면 DB를 조회한다.
이런식으로 엔티티를 직접 조회하게 되면 조회한 엔티티 사용유무에 관계없이 DB를 조회하게 된다.

엔티티를 실제 사용시점까지 DB조회를 미루고 싶다면 EntityManager.getReference()를 사용하면 된다. 이 메소드를 호출할 때 JPA는 DB를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신 DB접근을 위임한 프록시 객체를 반환한다.

프록시의 특징

프록시 클래스는 실제 클래스를 상속받아 만들어진 것이므로 실제 클래스와 겉 모양이 같다. 사용하는 입장에서
이게 진짜 객체인지 프록시 객체인지 구분하지 않고 사용해도 된다.

실제 객체의 참조를 보관하기 때문에 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

프록시의 특징 7가지

  • 프록시 객체는 처음 사용할 때 한번만 초기화된다.
  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 DB를 조회할 필요가 없으므로 프록시를 호출해도 실제 엔티티를 반환한다.
  • 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 그래서 준영속 상태의 프록시를 초기화하면 org.hibernate.LazyInitializationException 예외 발생시킨다.

프록시 객체 초기화

프록시 객체는 실제 사용될 때 DB를 조회해서 실제 엔티티 객체를 생성하는데 이것이 프록시 객체 초기화이다.

초기화 과정은 다음과 같다.

프록시와 식별자

엔티티를 프록시로 조회할 때 식별자(PK) 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.
프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 메소드를 호출해도 프록시를 초기화하지 않는다. 단, 엔티티 접근 방식을 프로퍼티(@Access(AccessType.PROPERTY))로 설정한 경우에만 초기화하지 않는다. 접근방식을 필드로 설정하면 JPA는 실행한 메소드가 그것만 조회하는 것인지 아니면 부가적인 기능이 있는지를 알지 못하기에 프록시 객체를 초기화한다.

프록시 확인

JPA에서 isLoaded(Object entity) 를 사용하여 프록시 인스턴스의 초기화 여부를 확인할 수 있다.

즉시 로딩과 지연 로딩

프록시 객체는 연관된 엔티티를 지연로딩 할때 주로 사용된다.

JPA의 조회시점 두가지 방법

  • 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회
    • 설정 방법 : @ManyToOne(fetch = FetchType.EAGER)
  • 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회한다.
    • 설정 방법 : @ManyToOne(fetch = FetchType.LAZY)

즉시 로딩

즉시 로딩(EAGER Loading)은 위에서 본것 처럼 FetchType을 EAGER로 설정해준다. 위 코드로 보면 멤버를 조회하는 순간 팀도 함께 조회한다.
두 테이블을 조회하기에 조회쿼리를 2번 실행할 것이라고 예상하지만, 대부분의 JPA 구현체는 즉시 로딩을 최적화 하기 위해 가능하면 조인 쿼리를 사용한다.

nullable 설정에 따른 조인 전략

  • @JoinColumn(nullable = true) : NULL 허용(기본값), 외부 조인 사용
  • @JoinColumn(nullbale = false) : NULL 허용하지 않음, 내부 조인 사용

지연 로딩

지연 로딩(LAZY Loading)을 사용하려면 FetchType을 LAZY로 설정해준다. 위 코드로 보면 find를 호출할 시에 멤버만 조회하고 팀은 조회하지 않지만 조회한 회원의 team 멤버변수에 프록시 객체를 넣어둔다.
반환된 팀 객체는 프록시 객체이므로 실제 사용될 때까지 데이터 로딩을 미룬다. 이것을 지연 로딩이라 한다.

즉시 로딩, 지연 로딩 정리

  • 지연 로딩 : 연관된 엔티티를 프록시로 조회한다. 프록시를 실제 사용할 때 초기화 하면서 DB를 조회한다.
  • 즉시 로딩 : 연관된 엔티티를 즉시 조회한다. Hibernate는 가능하면 SQL조인을 사용해서 한번에 조회한다.

프록시와 컬렉션 래퍼

Hibernate는 엔티티를 영속 상태로 만들 때, 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 Hibernate가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라고 한다.
엔티티를 지연 로딩하면 프록시 객체를 사용해서 지연 로딩을 수행하지만 주문내역 같은 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리해준다.

JPA 기본 페치 전략

fetch 속성의 기본 설정값은 다음과 같다.

  • @ManyToOne, @OneToOne : 즉시 로딩(FetchType.EAGER)
  • @OneToMany, @ManyToMany : 지연 로딩(FetchType.LAZY)

책에 나와있는 추천하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것 이라고 한다. 그리고는 실제 사용하는 상황을 봐서 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화 하면 된다고 한다.

컬렉션에 FetchType.EAGER 사용 시 주의점

  • 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
    • 일대다 조인은 결과 데이터가 다(N) 쪽에 있는 수만큼 증가하기 때문에 너무 많은 데이터를 반환할 우려가 있다.
  • 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.

Eager 설정과 조인 전략

  • @ManyToOne, @OneToOne
    • optional = false : 내부 조인
    • optional = true : 외부 조인
  • @OneToMany, @ManyToMany
    • optional = false : 외부 조인
    • optional = true : 외부 조인

영속성 전이: CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이기능을 사용하면 된다. JPA는 CASCADE 옵션으로 영속성 전이를 제공한다. 영속성 전이를 사용하면 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있다.

JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.

영속성 전이: 저장

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<>();
}

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Child {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    private Parent parent;
}

@Test
void 영속성전이테스트(){
    tx.begin();

    Child child1 = new Child();
    Child child2 = new Child();

    Parent parent = new Parent();

    child1.setParent(parent); //연관관계 추가
    child2.setParent(parent); //연관관계 추가
    parent.getChildren().add(child1);
    parent.getChildren().add(child2);

    em.persist(parent);
    em.flush();

    tx.commit();
}

위 코드처럼 부모를 영속화할 때, 자식도 함께 영속화하기 위해 cascade PERESIST 옵션을 설정했다.
이렇게하면 한번에 영속화 가능하다.

영속성 전이는 연관관계 매핑하는 것과는 아무 관련이 없다. 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐이다.

영속성 전이 : 삭제

삭제도 마찬가지로 영속성 전이 사용 가능하다. CasCadeType.REMOVE로 설정하여 부모 엔티티를 삭제하면 연관된 자식 엔티티들도 같이 삭제된다.

remove설정을 하지않고 부모를 삭제하면 부모 엔티티만 삭제가 된다. 그런데 이 부모를 삭제하는 순간 외래키 제약조건 때문에 DB에서 외래키 무결성 예외가 발생하게 된다.

CASCADE 종류

  • ALL : 모두 적용
  • PERSIST : 영속
  • MERGE : 병합
  • REMOVE : 삭제
  • REFRESH : refresh
  • DETACH : detach

cascade 옵션은 여러 속성을 같이 사용할 수 있다.

참고❗️ PERSIST, REMOVE는 persist, remove메소드를 실행할때 전이가 바로 발생하지 않고 flush를 호출할 때 전이가 발생한다.

고아 객체

JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이걸 고아 객체 제거라고 한다. 이 기능을 사용해서 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제하도록 할 수 있다.

orphanRemoval = true 옵션을 설정하면 컬렉션에서 엔티티를 제거할 때 DB의 데이터도 삭제된다. 마찬가지로 이 제거 기능도 영속성 컨텍스트를 flush할때 적용되므로 flush시점에 DELETE SQL이 실행된다.

모든 엔티티를 제거하려면 컬렉션을 비워주면 된다.

고아 객체 정리

고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다. 그렇기에 이 기능은 한군데에서만 참조할 때 사용해야 한다. 만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생할 수 있다. 이런 이유때문에 orphanRemoval 옵션은 @OneToMany, @OneToOne에만 사용할 수 있다.

고아 객체 제거에는 기능이 또 한가지가 있는데, 개념적으로 볼 때 부모를 제거하면 자식은 고아가 된다. 따라서 부모를 제거하면 자식도 같이 제거된다.

영속성 전이 + 고아 객체, 생명주기

여태 배운 두 옵션을 (persist + orphanRemoval = true) 같이 사용하면 부모 엔티티를 통해 자식의 생명주기를 관리할 수가 있다.

Parent parent = em.find(Parent.class, parentId);
parent.addChild(child1);

//delete
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(removeObject);

정리

이번 포스팅에서는 프록시의 동작 원리에 대해 학습하고 즉시 로딩 그리고 지연 로딩에 관해 알아보았다.

주요 내용

  • JPA구현체들은 객체 그래프를 맘껏 탐색할 수 있도록 지원하는데 이때 프록시 기술이 사용된다.
  • 객체 조회할 때 연관된 객체를 즉시 로딩하는 것이 즉시 로딩, 지연해서 로딩하는 것이 지연 로딩
  • 객체를 저장하거나 삭제할 때 연관된 객체도 함께 저장하거나 삭제할 수가 있는데 이것을 영속성 전이라고 한다.
  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하려면 고아 객체 제거 기능을 사용하면 됨
728x90

'JPA' 카테고리의 다른 글

[JPA] 객체지향 쿼리 언어 - 1  (0) 2022.08.05
[JPA] 값 타입  (0) 2022.08.05
[JPA] 고급 매핑  (0) 2022.08.04
[JPA] 다양한 연관관계 매핑  (0) 2022.08.04
728x90

시간변경

서버 하나를 구축하면 항상 시간이 UTC로 설정되어 매번 바꿔주어야 하는 번거로움을 피하기 위해 이 포스팅을 쓴다.

date

ls -l /etc/localtime

sudo rm -rf /etc/localtime

sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

timedatectl

이렇게 하면 KST시간으로 변경된것을 확인할 수 있다.

이걸 왜 쓰냐면

Tomcat 시간이 당연히 로컬 Linux의 시간을 따라가는줄 알았는데
시간이 변경이 되질 않아서 DB 시간이 맞지 않았다.

ubuntu(linux)tomcat의 시간은 다르게 흐른다.
그렇기 때문에 이 점 유의해서 실행 시켜야 한다.

아래는 Tomcat시간 변경이다.
참고로 setenv.sh는 기본으로 존재하지 않는 파일이므로 생성해야 한다.

start.sh는 catalina.sh를 부르고 catalina.sh는 기본적으로
setenv.sh가 있으면 읽고 없으면 안읽는 구조로 되어있다.

#!/bin/bash

vi /usr/share/tomcat8/bin/setenv.sh
export CATALINA_OPTS=-Duser.timezone=GMT+09:00

이렇게 해주고 나서

sudo service tomcat9 stop
sudo service tomcat9 start

재시작을 해주면 이제 시간이 정상적으로 들어가게 된다.

728x90

+ Recent posts