[JPA] - 값 타입

2022. 12. 29. 09:02·Spring/JPA
728x90
반응형

<< JPA에서 제공하느 값 타입의 종류, 임베디드 타입에 대해 인지하고, 불변객체를 왜 사용하는지, 언제 어떻게 사용하는지 알아보자>>

 

1) 엔티티 타입

 - @Entity 로 정의하는 클래스 객체

- 데이터가 변해도 식별자로 지속 추적 가능 > PK 값으로 구분 가능

 

2) 값 타입

- 변경시 추적이 어려운, 단순한 값으로 사용되는 자바 기본 타입 / 객체

 

 

값타입 

기본값 / 임베디드 타입 / 컬렉션 타입 으로 나뉨 >> 값타입은 항상 불변객체로 한다라고 그냥 생각해도 됨 (아래 나옴)

 

기본값 - 생명 주기가 엔티티에 의존된다. 

 

기본값, 기본 타입 은(int, double) 은 공유가 안된다. 

int a= b

int b= a

a = 20;

출력해보면 a =20, b = 10 이 된다. 

 

하지만 클래스 (래퍼 클래스처럼) 

 

Integer a= new Integer(10);

Integer b = a;

a.setValue(20);  // 이런거 없는데 한다고 치자

출력해보면 a=b=20 이라고 한다.

레퍼런스가 넘어가서 같은 인스턴스를 a,b 가 공유하고, 그 주소를 받아서 사용하기 때문이다. 

 

 

<< 임베디드 타입 개요 >>

 

Member - id, name, startDate, endDate, zipcode, street, city 의 값들을 가지고 있다!

>> 뭔가 이상함. 이렇게 말하기 보단

멤버는 이름 / 근무 기간 / 집 주소에 대한 값을 가지고 있다! 라고 표현하는게 맞음. 

Member - id, name, Period workPeriod, Address homeAddress 이렇게 표현하는게 객체지향상 맞음!

 

JPA 에서는 이런 경우를 , Embedded Annotation 을 통해서 한다. 

 

>>>> 장점 : 당연히 객체지향 특징처럼 재사용이 가능하다. 응집도가 높다. 해당 값타입에서만 할 수 있는 논리 메소드를 만들 수 있다. ex) Period.isWorking() 등. 

 

<4min 40 초 대 설명>

회원테이블은 바뀔게 없음 >> 하지만 JPA Entity 클래스상에서 바뀌는 것임. 매핑을 해주는 방식에 차이가 있음. (이런게 객체와 테이블의 차이임!) 

 

@Entity
@Data
public class Employees {

    @Id
    @GeneratedValue
    @Column(name = "employee_id")
    private Long id;

    private String username;
    
    // 기간
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    // 주소
    private String city;
    private String street;
    private String zipcode;
    
}

 다음과 같이 정의되었다고 해보자. DB를 실행해보면 그냥 생각하는대로 들어가 있는 것을 볼 수 있다. 

 기간을 Period period로, 주소를 Address address 로 변경후 생성을 해보자. 

@Data
@Embeddable
@NoArgsConstructor
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;
}

@Data
@Embeddable
@NoArgsConstructor
public class Address {
    private String city;
    private String street;
    private String zipcode;
}

기본생성자는 필수이며, Embeddable annotation 을 사용할 수 있다. 그리고 Employee 클래스를 다음과 같이 바꿀 수 있다.

@Entity
@Data
public class Employees {

    @Id
    @GeneratedValue
    @Column(name = "employee_id")
    private Long id;

    private String username;

    @Embedded
    private Period workPeriod;
//    private LocalDateTime startDate;
//    private LocalDateTime endDate;

    @Embedded
    private Address homeAddress;
//    private String city;
//    private String street;
//    private String zipcode;

}

DB를 실행해보면, 임베디드 클래스를 만들기 전과 테이블 상 아무 차이가 없음을 알 수 있다. 

이게 제일 중요함 ==> 테이블은 동일함. 

 

> 하지만 테이블에 좀 더 객체지향스럽게 매핑이 가능해진 것!

> 매핑 테이블 수보다 내부 객체지향 클래스 수가 더 많을 수록 활용도가 높은 것!

 

만약에 두개를 써야 한다면? 

 

public class Employees {
    ...
    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;
    private Address workAddress;
}

이거 그대로 돌려보면 에러가 발생함. 칼럼명들이 다 똑같은 상태에서 동일하게 넣으려 하기 때문. 

@AttributeOverride 를 사용하면 됨. 

 

public class Employees {
    ...
    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;
    
    @Embedded
    @AttributeOverrides({ // 동일 칼럼 하나하나에 대해 다 지정해줘야 함
            @AttributeOverride(name = "city", column = @Column(name = "work_city")),
            @AttributeOverride(name = "street", column = @Column(name = "work_street"))
    })
    private Address workAddress;
}

동일한 칼럼 하나하나에 대해서 새로운 칼럼명들을 위와 같이 지정해주면 됨. 

 

 

 

<< 불변 객체 >>

 

임베디드 타입 같은 값을 여러 엔티티 상에서 공유하면 위험하다. 다음과 같은 예제를 보자. 

 

@Test
@DisplayName("Embedded Type Dangerous")
void test2() {
    tx.begin();
    try {
        Address address = new Address("city", "street");

        Employees emp1 = new Employees();
        emp1.setUsername("EMP1");
        emp1.setHomeAddress(address);

        Employees emp2 = new Employees();
        emp2.setUsername("EMP2");
        emp2.setHomeAddress(address);

        em.persist(emp1);
        em.persist(emp2);

        // 아 emp2 의 street 은 달랐지 하고 바꾸려 함
        emp2.getHomeAddress().setStreet("Another Street");
            
        tx.commit();
    } catch (Exception e) {
        e.printStackTrace();
        tx.rollback();
    } finally {
        em.close();
    }
    emf.close();
}

저걸 의도해서 같이 쓰고 싶은 거면, 값타입을 쓰면 안됨. 엔티티를 사용해야 하는 거임. 

위와 같이 공유를 하면 안되고, 복사해서 만들어서 사용해야 함. 

try {
    Address address = new Address("city", "street");

    Employees emp1 = new Employees();
    emp1.setUsername("EMP1");
    emp1.setHomeAddress(address);

    Address copiedAddress = new Address(address.getCity(), address.getStreet());
    Employees emp2 = new Employees();
    emp2.setUsername("EMP2");
    emp2.setHomeAddress(copiedAddress);

    em.persist(emp1);
    em.persist(emp2);

    // 아 emp2 의 street 은 달랐지 하고 바꾸려 함
    emp2.getHomeAddress().setStreet("Another Street");

    tx.commit();
}

다음과 같이 새로운 객체를 생성하고 복사값을 넣어준 뒤로 세팅을 해주게 되면, 나중에 바꿨을 때도 아무 문제가 없다. 

 

자 한번만 다시 복습하자면, 기본 값 타입은 공유시 값을 복사해서 넣어주고, 객체 값 타입은 공유시 참조값 (주소)  넣어준다. 이래서 객체 값타입은 같이 변경이 될 수 있는 거다. 

> 이건 막을 수가 없음, 위에 복사 객체를 만드는것도 조금 이상해보이기도 하고, 깜빡할 확률이 굉장히 높음. 

 

 따라서, 아예 못하게 해버려야 한다 . 그런 설계를 불변 객체라고 한다. 

값 타임은 불변 객체로 설계를 해야 함. 1) 생성자로만 값을 수정하고, Setter를 만들지 않는다. 2) Setter를 Private 으로 선언한다 (내부 활용시에만 바꿀 수 있도록) >> 그래야 위와 같이 해줬을 때 개발자가 "뭔가 잘못되었다", "이렇게 하면 안되는구나" 라는걸 인지할 수 있음. >> 참고로, String 과 Integer 는 대표적인 Java 가 제공하는 불변객체이다. 

 

이렇게 하면 (위에선 복사객체?를 만들었기 때문에 괜찮지만) 위에 emp2.getHomeAddress().setStreet("~~"); 은 에러가 발생한다. Address 는 불변객체이기 떄문이다.

 

그렇다면 바꾸고 싶은디 어떻게 바꿔야 함?? Address 하나만 써도 못바꿈!!

다음과 같이 바꾸면 됨. 

try {
    Address address = new Address("city", "street");

    Employees emp1 = new Employees();
    emp1.setUsername("EMP1");
    emp1.setHomeAddress(address);
    
    // 다시 만들어야함 ㅋㅋㅋㅋㅋ
    Address newAddress = new Address("NEW CITY", address.getStreet());
    emp1.setHomeAddress(newAddress); // 이건 당연히 됨. Address가 불변객체 
    
    em.persist(emp1);
    tx.commit();
}

다시 쓰면 됨 ㅋㅋㅋ. 이렇게 하는게 맞음. 

 

<< 값 타입의 비교>> 

 

int a = 10, int b = 10 >> a == b 시 TRUE (기본 값 타입은 그 내부 값이 같으면 같다고 취급) 

Address a = new Address("ㅈㅈ"), b = new Address("ㅈㅈ"), >> a ==b 시 당연히 False. 다른 객체이기 때문 ㅇㅇ (당연한거) 

 

객체랑 기본 값은 저장되는 위치가 다르기 때문에 비교되는 방식도 다름. 

 

-- 동일성 비교와 동등성 비교를 구분해야 함. 

동일성 (== 비교) , 동등성 (.eqauls() 비교) 

자 방금전에 한 a, b 비교, equals 로 하면 어떨까?

a.equals(b)  :  당연히 False 나옴. equals 비교는 기본이 == 비교이기 때문임. 

따라서 동등성 비교를 따로 구분 짓기 위해서, 동등성 비교를 구분하려는 객체 클래스 내에서 .equals 를 Overriding 해야 함. 

 

Address 내부의 equals 를 overriding 해보자. (Overriding 항목 보면 [Generate equals() and hashCode()] 라는 항목이 있음. IntelliJ에서 오버라이딩 하는거면 95% 동등성 확인하는 거임으로 자동으로 해줌. Address 에 대해서 equlas 를 만들어라라고 한다면, 다음과 같이 만들 것이고, 이는 내부 값들이 같으면 true를 반환하는 형식으로 짜여져 있음을 알 수 있다. 

...
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(city, address.city) && Objects.equals(street, address.street);
    }

    @Override
    public int hashCode() { // 이건 모른다면 나중에 알아보면 되는디, 그냥 hashMap 등 자료구조에서 효율적인 사용을 위한 것임. 알아보면 좋을 듯
        return Objects.hash(city, street);
    }
}

이 상태에서 위에 a.equals(b) 를 해본다면 이제서야 True 가 나온다. 오버라이딩 된 함수가 동등성 비교 함수로 바뀌었기 때문이다. String 이 == 가 아니라 equlas 를 쓰는 것을 권장하는 이유이다. 

 

임베디드 타입은 대부분 직접 다 해야하는데, 그런 경우 동일성 비교가 아니라 동등성 비교로 바꿔주는게 좋다. 근데 생각보다 .equals() 쓸 일은 많진 않음 ㅋㅋ. 그냥 하다가, 뭔가 임베디드 타입 같음을 비교하고 있는 것 같을 떄 아! 그거가 있었지! 하면서 생각나서 동등성 비교할 수 있으면 성공임 ㅇㅇ

 

 

<< 값 타입 컬렉션>>

 

그냥 말그대로 컬렉션인데 값 타입이 들어가는 컬렉션임. (List<Address> , List<Period> 등) 

값 타입을 하나 이상 저장할 때 사용 

List<Address>, Set<String> 등등

 

컬렉션은 기본적으로 1:N 관계이기 때문에 편법(All String 변환 따위)을 쓰지 않는 한 같은 테이블에 저장시킬 수 없기 때문에, @ElementCollection annotation 을 사용하여 이것은 [값 타입 컬렉션] 이다라는걸 말해주고, 

@CollectionTable 을 통해 그 값타입을 따로 컬렉션으로 만들어 줄건데, 어떻게 만들어줄건지 지정해주면 된다. 가령, 위 Employee Class 를 다음과 같이 수정해볼 수 있다. 

 

@Entity
@Data
public class Employees {

    @Id
    @GeneratedValue
    @Column(name = "employee_id")
    private Long id;

    private String username;

    @Embedded
    private Address homeAddress;

    /* 컬렉션 예제 */
    @ElementCollection
    @CollectionTable(name = "favorite_food", joinColumns =
        @JoinColumn(name = "member_id")
    )
    @Column(name = "food_name")
    private Set<String> favoriteFoods = new HashSet<>();


    @ElementCollection
    @CollectionTable(name = "address", joinColumns =
        @JoinColumn(name = "member_id")
    )
    private List<Address> addressHistory = new ArrayList<>();
}

이 상태에서 실행하면, favorite_food 라는 테이블과 address 라는 테이블이 생겨있고, 각각 String, Address(임베디드 저장되듯이) 에 해당하는 칼럼들이 형성된 모습을 확인할 수 있다. 그리고 Join 할 키들을 넣어줄 수 있다. (member_id) 

 

사용 예제를 살펴보자. 

{
..
try {

    Employees employees = new Employees();
    employees.setUsername("emp1");
    employees.setHomeAddress(new Address("city", "street"));
    employees.getFavoriteFoods().add("치킨");
    employees.getFavoriteFoods().add("피자");
    employees.getFavoriteFoods().add("족발");

    employees.getAddressHistory().add(new Address("old1", "old1"));
    employees.getAddressHistory().add(new Address("old2", "old2"));
    em.persist(employees);
    tx.commit();
} catch (Exception e) {
..
}

그냥 생각하듯이 해당 클래스의 get을 통해서 adding 을 해주면 다음과 같이 DB에 잘 들어가 있는 모습을 확인할 수 있따. 

 

<<DB 모습>>

 

>> collection 만 하니까 다른 테이블인데도 불구하고 favorite_food 와 address 가 같이 persist 됨. 라이프 사이클이 같이 돌아감. <값 타입 컬렉션> 이기 때문. 값타컬도 생명주기가 Employee 에 소속됨. (String 도 값타입, Long 도 값 타입. 모두 Employee 에 의존하기 때문에, 컬렉션 값타입도 마찬가지임) Cascade All 처럼 알아서 들어가 있음. (고아 객체 제거 기능까지 다 들어가 있음) 

 

이번엔 저장 말고 조회에 대해 살펴보자. 다음과 같이 Test를 수정한다. 

 

{ ...
try {
    // ... 동일
    
    em.flush();
    em.clear();

    System.out.println("==================START ==================");
    Employees employees1 = em.find(Employees.class, employees.getId());

    tx.commit();
} catch (Exception e) {
    ...
}

Query 날라가는 것을 확인해보면, Employee 에 대한 값들만 가져오는 것을 알 수 있음.  컬렉션들은 다 ~ 지연 로딩이란 뜻. 

 

그 이후로 가지고 오고 출력해보자. 

System.out.println("==================START ==================");
Employees foundEmployee = em.find(Employees.class, employees.getId());
for (Address address :
        foundEmployee.getAddressHistory()) {
    System.out.println("address.getCity() = " + address.getCity());
}

그러고 쿼리 날라가는걸 보면, 그 이후로 Address에 대한 select 문이 또 나가고, where member_id = 조회 유저. 이 쿼리가 날라가는 것을 확인할 수 잇다. 그렇게 값타입 컬렉션도 지연로딩 전략을 사용하여 프록시를 사용하는 것을 알 수 있음. (들어가보면 @ElementCollection = Fetchtype.LAZY 로 default 설정되어 있음) 

 

이제 수정을 보자. 이 부분이 그나마 이해가 필요.

위에서 바로 말했지만, 값 타입은 ALL IMMUTABLE 이다. 불변 객체들임. 

 

==== 잠깐 ====

값 타입은 기본타입 / 임베디드 타입 / Collection타입 으로 나뉜다고 했다 .

값타입이 ALL IMMUTABLE 이란 무슨 소리냐

그냥 다 함부로 set 하면 안된다는 소리다. 

즉 제일 처음에 말했던 것으로 돌아간다 -> Entity 에 대해서는 함부로 Setter를 열어두면 안된다. String 같은거야 값 자체가 그 value 니까 set을 써도 된다 하지만, (지금 말하는건 관례적인 set을 말하는거임 (this.~  = ~ ; ), 이렇게 수정하면 그 값을 또다른 어디서 참조하고 있으면 같이 변경되는 에러가 발생할 수도 있다. (사실 어떤 에런지 본 적이 없으니 정확히 모르겠음) 

>> 기본 값 타입에 대해선 그 값을 변경해 줌으로써 변경이 충분하긴 하니까 set을 하는거랑 큰 차이 없는 행위지만, 임베디드 타입 같은 경우 반드시 새로 만들어서 넣어줘야 한다) (뭐 이렇게 내 생각을 적고 보니까 setter가 크게 문제있는 건 아닌듯. 다만 어떤과정을 통해서 set을 해줘야하는지 정확,. 아주 정확하게 알고 있어야 할 뿐) 

===============

 

따라서, 수정할 떄 다음과 같이 해볼 수 있다. 

 

@Test
@DisplayName("Embedded Type Collection Usage : Save and Update")
void test4() {
    ...
    try{
    Employees foundEmployee = em.find(Employees.class, employees.getId());

    // 복습!! City 바꾸고 싶음
    // foundEmployee.getHomeAddress().setCity("new City"); 이렇게 바꾸면 안됩니다.
    Address preAddress = foundEmployee.getHomeAddress();
    foundEmployee.setHomeAddress(new Address("new City", preAddress.getStreet())); // 통째로 바꿔야 함

    // 이제 컬렉션 UPDATE
    // Set<String> 을 치킨 > 한식으로 바꾸고 싶음
    foundEmployee.getFavoriteFoods().remove("치킨");
    foundEmployee.getFavoriteFoods().add("한식");
    // String 도 결국 Immutable 성질을 적용시켜야 해서, 사실 UPDATE 따윈 없음
    // 삭제하고 추가, 결국 갈아끼우는 것임

    tx.commit();
    }catch(){
    ...
}

 

기본 값 타입 컬렉션은 위처럼 해줄 수 있다. 그렇다면 임베디드 값 컬렉션 수정은 어떻게 할까? 갈아끼워야 한다고 했다. 

 

{ ...
    // 이젠 임베디드 컬렉션 Update : EQUALS 가 매우 중요한 타이밍
    // 기존 컬렉션 내의 특정 객체를 그대로 넣어줌으로써 삭제하라고 하는 것
    // 이제 .Equals 오버라이딩이 제대로 안되어 있으면 개판 나는것
    foundEmployee.getAddressHistory().remove(new Address("old2", "old2"));
    // 새거 넣어준다.
    foundEmployee.getAddressHistory().remove(new Address("new City", "old2"));
    tx.commit();
    ...
}

아직 끝이 아니다. 이 원리를 파악하려면, 제대로 SQL 까지 봐야 한다. SQL 문이 다음과 같이 나가는 것을 볼 수 있다. 

 

...
Hibernate: 
    /* delete collection jpa.demo.type.Employees.addressHistory */ delete 
        from
            address 
        where
            member_id=?
...
Hibernate: 
    /* insert collection
        row jpa.demo.type.Employees.addressHistory */ insert 
        into
            address
            (member_id, city, street) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert collection
        row jpa.demo.type.Employees.addressHistory */ insert 
        into
            address
            (member_id, city, street) 
        values
            (?, ?, ?)
...

기존 Address 테이블에서 현재 묶여있는 employee_id 값을 가진 애들을 모~~~~~~~~두 삭제하고, 그 다음에 현재 그 Employee 가 들고 있는 객체들을 다시 다 넣어준다. 

 

느낌이 와야겠지만. 쿼리가 문제가 있는 거다. 결론적으론 맞게 나왔지만, 이거 이런식으로 동작하면 진짜 overwhelming 이 큰거다. (이 느낌 온다면 잘 공부하고 있는 거인듯)

 

왜 이런 일이 일어나는 걸까??

 

값은 변경하면 추적이 어려움. 따라서 값 컬렉션에 변경 사항이 생기면 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다 .

 

>>>>>>>> 결론 ㅋㅋㅋㅋㅋㅋ 쓰지 말아라 ^_^

 

연관 컬렉션은 식별자가 없기 때문에, DB에서 어떤 방식으로든 추적하기가 굉장히 어려움. (임베디드 말하는게 아니라 임베디드 컬렉션 말하는거임!!!) > 임베디드는 원래 테이블이 없기 때문에 id 가 없음. @ElementCollection 이거가 문제라는거임. 

 

해결방안이 있긴 한데, 그것도 문제가 있음. >> 걍 쓰지마. 그냥 쓰지말라면 쓰지마. 이렇게 복잡하게 갈거면 설계를 잘못한거야. 걍 쓰지마 ㅇㅇ

 

이정도로 갈꺼면 Entity 를 새로 만들어서 PK를 쥐어주는게 맞음. 그리고 1:N관계로 풀어내는게 맞음. 시발아. 당연한거 아니냐. 

 

즉, 이렇게 대안을 마련할 수 있다. 

 

@Entity
public class AddressEntity {
    @Id
    @GeneratedValue
    private Long id;
    
    @Embedded
    private Address address;
}

@Entity
public class Employees{
    ...
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "member_id")
    private List<AddressEntity> addressHistory = new ArrayList<>();
    ...
    
}

이렇게 들어오면 맘대로 수정해도 됨. 

실무에서는 값타입 컬렉션 대신에 일대다 관계를 우선적으로 고려를 해야한다. 진짜 조오옹ㄴ나 단순한거면 써도 되는데, 그럴바엔 String 같은 경우 Enum 이용하는게 나을듯.. 

 

 

정리

- 값타입은 정말 값타입을 써야겠다 할 때만 사용해라

- 엔티티와 값타입을 혼동하면 안됨. >>> 엔티티를 값타입으로 만들면 안됨!!! (오히려 배워서 더 헷갈린거 같긴함 ㅋㅋ)

- 식별자가 필요하고, 추적해야할 필요성을 느낀다면 진짜 무조건 엔티티임. 

 

 

 

 

728x90
반응형

'Spring > JPA' 카테고리의 다른 글

[JPA] JPQL 쿼리 2 (FETCH JOIN의 등장)  (0) 2023.01.03
[JPA] JPQL 쿼리  (0) 2022.12.29
[JPA] 프록시와 연관관계 - 2  (0) 2022.12.28
[JPA] 프록시와 연관관계 정리  (0) 2022.12.26
[JPA] 연관관계 매핑 -2  (0) 2022.12.26
'Spring/JPA' 카테고리의 다른 글
  • [JPA] JPQL 쿼리 2 (FETCH JOIN의 등장)
  • [JPA] JPQL 쿼리
  • [JPA] 프록시와 연관관계 - 2
  • [JPA] 프록시와 연관관계 정리
문케이크
문케이크
    반응형
  • 문케이크
    누구나 개발할 수 있다
    문케이크
  • 전체
    오늘
    어제
    • 전체 보기 (122)
      • CS 이론 (13)
        • 운영체제 (8)
        • 네트워크 (2)
        • 알고리즘 (0)
        • Storage (3)
      • Spring (26)
        • Spring 기본 (12)
        • Spring 심화 (0)
        • JPA (11)
        • Spring Security (3)
      • 리액티브 (0)
        • RxJava (0)
      • SW 설계 (14)
        • OOP (0)
        • UML (3)
        • OOAD (0)
        • Design Pattern (11)
      • Java (8)
      • 웹 운영 (17)
        • AWS (15)
        • 운영 구축 (2)
      • Testing (3)
        • Unit (3)
      • Extra (3)
        • API 적용 (1)
      • 인프라 기술 (5)
        • Kubernetes (2)
        • Elasticsearch (3)
      • Logging (7)
        • Spring (5)
        • 인프라 (2)
      • 일상 (2)
        • 음식점 리뷰 (2)
        • Extra (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 문케이크의 블로그
  • 인기 글

  • 태그

    decorator
    analyzer
    SRP
    n+1
    spring container
    Configuration
    김영한
    di
    k8s
    elasticsearch
    junit
    Setter
    디자인 패턴
    mockito
    lazy loading
    OOP
    Spring
    GoF
    JPA
    Design Pattern
    단위테스트
    Composite
    lombok
    spring boot
    OOAD
    객체지향
    composition
    runtime exception
    BEAN
    Java
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
문케이크
[JPA] - 값 타입
상단으로

티스토리툴바