엔티티 클래스에 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.class, 1); | 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.class, 1); assertThat(1, is(member.getId())); phone = em.find(LPhone.class, 1); 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.class, 1); 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 |
'프로그래밍' 카테고리의 다른 글
linux 장비 원격 실행 프로그램 (0) | 2018.05.08 |
---|---|
spring boot swagger2 사용하기 (0) | 2018.02.01 |
16. [JPA] Bulk Insert (0) | 2017.12.14 |
15. [JPA] Spring Data JPA (0) | 2017.12.14 |
19. [JPA] 트랜잭션 테스트 (0) | 2017.11.27 |
18. [JPA] N+1 문제 (fetch 조인 해결 방안) (0) | 2017.11.24 |
17. [JPA] N+1 문제 (@BatchSize 해결 방안) (0) | 2017.11.23 |
2. Spring Cloud를 이용한 MSA 구축하기 - config server (0) | 2017.09.22 |