20. [JPA] lombok 사용 시 주의사항

프로그래밍|2017. 11. 28. 15:32

엔티티 클래스에 lombok을 사용하면 StackOverflowError 오류가 발생할 수 있다.


다음과 같은 엔티티 연관 관계가 있다고 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Data
@Entity
class LMember {
 
    @Id
    @Column(name = "MEMBER_ID")
    private int id;
 
    private String name;
 
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "lMember", cascade = CascadeType.ALL)
    private List<LPhone> phoneList = new ArrayList<>();
 
    @OneToOne(mappedBy = "lMember", cascade = CascadeType.ALL)
    private LHomeAddress lHomeAddress;
 
    public void addPhone(LPhone phone) {
        this.phoneList.add(phone);
        phone.setLMember(this);
    }
 
    public void setlHomeAddress(LHomeAddress homeAddress) {
        this.lHomeAddress = homeAddress;
        homeAddress.setLMember(this);
    }
}
 
@Data
@Entity
class LPhone {
 
    @Id
    @Column(name = "PHONE_ID")
    private int id;
 
    private String number;
 
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private LMember lMember;
}
 
@Data
@ToString(exclude = "lMember")
@Entity
class LHomeAddress {
 
    @Id
    @Column(name = "HOME_ADDRESS_ID")
    private int id;
 
    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private LMember lMember;
}
cs


다음과 같이 LPhone 엔티티 오브젝트를 리턴받았다.

1
LPhone phone = em.find(LPhone.class1);
cs


여기서 다음의 코드를 만나면 StackOverflowError 오류가 발생하게 된다.

1
System.out.println(phone.getLMember());
cs


특이하게도 phone.getLMember() 이와 같이 호출하면 오류가 발생하지 않는다.


왜 그럴까?

왜 System.out.println(phone.getLMember()); 인 경우에만 오류가 발생하는가?


이유를 알아보도록 하자.

println 메서드를 일단 들여다 보자.

음~ 뭐 별 거 없구만.

1
2
3
4
5
6
7
public void println(Object x) {
    String s = String.valueOf(x);
    synchronized (this) {
        print(s);
        newLine();
    }
}
cs


valueOf 메서드를 들여다 보자.

보이는가?

obj.toString(); 을 호출하는 것이~

1
2
3
public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}
cs

즉, System.out.println(phone.getLMember()); 코드의 상세 동작은 LMember 엔티티의 toString() 메서드를 호출한다는 것이다.


롬복에서 자동으로 생성해 주는 toString() 메서드는 해당 엔티티가 가지고 있는 모든 속성들이 작성되어져 있다.

다음과 같이 말이다.

1
2
3
4
5
6
7
8
@Override
public String toString() {
    return "LPhone{" +
            "id=" + id +
            ", number='" + number + '\'' +
            ", lMember=" + lMember +
            '}';
}
cs


toString() 메서드가 호출되어 로그에 기록될 때 lMember 엔티티의 toString() 메서드를 호출하게 되고, 또 lMember의 toString()에는 LPhone 엔티티가 정의되어 있기에 LPhone 의 toString() 메서드를 호출하게 될 것이다.


정리하면

1
2
3
4
5
6
7
1. System.out.println(phone.getLMember()); 코드 실행
2. LPhone 엔티티의 toString() 메서드 호출 (toString 메서드 안에 LMember 정의되어 있음)
3. LMember 엔티티의 toString() 메서드 호출 (toString 메서드 안에 LPhone 정의되어 있음)
 .
 .
 .
2~3번을 무한 반복하게 되면서 StackOverflowError 오류 발생
cs


[해결방안]

1
2
3
1. 롬복 제거
2. 롬복의 @Getter, @Setter만 사용하고 @toString을 제정의한 뒤 엔티티 연관관계로 설정되어 있는 속성을 제거
3. @ToString(exclude = "lMember") 옵션을 추가
cs



[전체 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class LombokTest {
 
    @PersistenceContext
    private EntityManager em;
 
    @Test(expected = StackOverflowError.class)
    @Transactional
    public void 롬복_무한루프_오류() {
 
        // dummy data
        LMember member = new LMember();
        member.setId(1);
        member.setName("nklee");
 
        LPhone phone = new LPhone();
        phone.setId(1);
        phone.setNumber("010-1111-1111");
        member.addPhone(phone);
 
        LHomeAddress lHomeAddress = new LHomeAddress();
        lHomeAddress.setId(1);
        member.setlHomeAddress(lHomeAddress);
 
        // save
        em.persist(member);
        em.flush();
        em.clear();
 
        // verify
        member = em.find(LMember.class1);
        assertThat(1, is(member.getId()));
 
        phone = em.find(LPhone.class1);
        assertThat(1, is(phone.getId()));
 
        // toString() 메서드 호출로 인해 StackOverFlowError 발생
        System.out.println(phone.getLMember());
 
        fail("java.lang.StackOverflowError 오류 발생!!");
    }
 
    @Test
    @Transactional
    public void 롬복_무한루프_해소() {
 
        // dummy data
        LMember member = new LMember();
        member.setId(1);
        member.setName("nklee");
 
        LHomeAddress lHomeAddress = new LHomeAddress();
        lHomeAddress.setId(1);
        member.setlHomeAddress(lHomeAddress);
 
        // save
        em.persist(member);
        em.flush();
        em.clear();
 
        // verify
        member = em.find(LMember.class1);
        assertThat(1, is(member.getId()));
 
        // StackOverFlowError 발생하지 않음
        // @ToString(exclude = "lMember") 설정 추가 후 해결
        System.out.println(member.getLHomeAddress());
    }
}
 
@Data
@Entity
class LMember {
 
    @Id
    @Column(name = "MEMBER_ID")
    private int id;
 
    private String name;
 
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "lMember", cascade = CascadeType.ALL)
    private List<LPhone> phoneList = new ArrayList<>();
 
    @OneToOne(mappedBy = "lMember", cascade = CascadeType.ALL)
    private LHomeAddress lHomeAddress;
 
    public void addPhone(LPhone phone) {
        this.phoneList.add(phone);
        phone.setLMember(this);
    }
 
    public void setlHomeAddress(LHomeAddress homeAddress) {
        this.lHomeAddress = homeAddress;
        homeAddress.setLMember(this);
    }
}
 
@Data
@Entity
class LPhone {
 
    @Id
    @Column(name = "PHONE_ID")
    private int id;
 
    private String number;
 
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private LMember lMember;
}
 
@Data
@ToString(exclude = "lMember")
@Entity
class LHomeAddress {
 
    @Id
    @Column(name = "HOME_ADDRESS_ID")
    private int id;
 
    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private LMember lMember;
}
cs


댓글()