해당 환경은 모두 Mac OS환경에서 진행되었습니다.


Lombok(jar) 설치



https://projectlombok.org/download에서 최신버전 혹은 원하는 버젼의 Lombok을 다운로드 받아 줍니다.









Lombok jar 실행


lombok이 설치된 경로로 들어가서 jar파일을 실행시켜줍니다.


java -jar $LOMBOK_DOWNLOAD_PATH/lombok.jar



만약 첫번째 이미지처럼 떴다면 로컬의 IDE를 찾을 수 없다는 alert창입니다. 그렇다면 2,3번째 이미지처럼 IDE의 ini파일의 경로를 적용시켜서 install/Update 버튼을 눌러준 후에 인스톨러를 종료시켜줍니다.


여기까지 따라왔다면 이클립스를 재시작해줍니다.





마지막 이클립스의 ini파일을 열어보면


아래와 같이 javaagent가 등록이 된것을 확인할 수 있습니다.


그리고 pom.xml에 방금 받은 lombok version의 라이브러리를 dependency 하면 런타임시점에 @Getter,@Setter등의 어노테이션이 해당 코드로 generate됩니다.




간단한 롬복 어노테이션




@NonNull
NullPointerException을 발생하지 않도록 미리 체크한다.

@Cleanup
자동 리소스 관리를 수행한다.: 안전하게 close()메소드를 호출한다.

@Getter / @Setter
getter/setter메소드를 자동으로 생성한다.

@ToString
lombok가 자동으로 toString 메소드를 생성해준다. 모든 필드들을 toString으로 노출시켜준다.

@EqualsAndHashCode
equals와 hashCode메소드를 자동으로 생성해준다. 객체의 필드들을 이용하여 구현하게 된다.

@NoArgsConstructor, @RequiredArgsConstructor and @AllArgsConstructor
생성자를 만들어 준다.
@NoArgsConstructor : 파라미터 없는 생성자를 만들어준다.
@RequiredArgsConstructor : not null필드나 final필드들을 아규먼트로 하는 생성자를 만든다.
@AllArgsConstructor : 모든 파라미터를 이용하여 생성자를 만든다.

@Data
@ToString, @EqualsAndHashCode, @Getter, @Setter, @RequiredArgsConstructor 를 모두 한번에 적용한 어노테이션이다.

@Value
불변 클래스들을 매우 쉽게 만든다.

@Builder
객체 생성을 빌더로 만들어준다.

@SneakyThrows
checked exceptions를 던진다. 이전에 thrown을 지정하지 않은 경우에 설정된다.

@Synchronized
synchronized가 올바르게 수행되도록 한다. locks를 볼 수 없을 것이다.

@Getter(lazy=true)
Getter을 생성한다. lazy하게.. 느린것이 좋은 것이다.

@Log
Logs를 사용할 수 있도록 해준다.




posted by 여성게
:
Search-Engine/Lucene 2019. 2. 2. 13:23

Lucene - 유사어,동의어필터(SynonymFilter)를 이용한 커스텀 Analyzer



Lucene에는 사용자가 입력한 질의 혹은 색인 할때의 토큰화 과정에서 여러가지 필터를 등록할 수 있다. 토큰의 종류는 아주 많다. StopFiler(불용어처리,불용어처리 단어의 리스트가 필요),SynonymFiler 등 의 필터들이 존재한다. 그 말은 단순히 토큰화된 텀들을 그대로 사용하는 것이 아니라 전처리,후처리를 필터를 이용해서 처리하여 토큰화된 텀에게 여러가지 효과?를 적용할 수 있는 것이다. 여기서는 간단히 유사어필터를 이용한 Custom한 분석기를 만들어 볼 것이며, 유사어 필터의 특징을 간단히 설명할 것이다.









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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
public class SynonymAnalyzerTest {
 
    
 
    
 
    public void testJumps(String text) throws IOException {
 
        System.out.println("Analyzing \"" +text+ "\"");
 
        System.out.println("\n");
 
        
 
        SynonymAnalyzer analyzer = new SynonymAnalyzer();
 
        
 
        String name = analyzer.getClass().getSimpleName();
 
        System.out.println("  "+name+"  ");
 
        System.out.print("    ");
 
        AnalyzerUtils.displayTokens(analyzer,text);
 
        
 
        System.out.println("\n");
 
        
 
    }
 
    
 
    
 
    public static void main(String[] args) throws IOException {
 
        // TODO Auto-generated method stub
 
        SynonymAnalyzerTest t = new SynonymAnalyzerTest();
 
        t.testJumps("나는 jumps 할거야");
 
    }
 
}
 
class SynonymAnalyzer extends Analyzer{
 
    
 
    @Override
 
    protected TokenStreamComponents createComponents(String fieldName) {
 
        // TODO Auto-generated method stub
 
        
 
        SynonymMap.Builder builder = new SynonymMap.Builder(true);
 
        builder.add(new CharsRef("JUPMS"), new CharsRef("점프,뛰다"), true);
 
        
 
        SynonymMap map = null;
 
        
 
        try {
 
            map=builder.build();
 
        }catch (Exception e) {
 
            // TODO: handle exception
 
            e.printStackTrace();
 
        }
 
        
 
        Tokenizer tokenizer = new StandardTokenizer();
 
        TokenStream filter = new LowerCaseFilter(tokenizer);
 
        filter = new SynonymFilter(filter,map,true);
 
        return new TokenStreamComponents(tokenizer,filter);
 
    }
 
    
 
}
 
class AnalyzerUtils{
 
    public static void displayTokens(Analyzer analyzer,String text) throws IOException {
 
        displayTokens(analyzer.tokenStream("content"new StringReader(text)));
 
    }
 
    
 
    public static void displayTokens(TokenStream stream) throws IOException {
 
        
 
        //텀 속성확인
 
        CharTermAttribute cattr = stream.addAttribute(CharTermAttribute.class);
 
        
 
        //위치증가값 속성 확인
 
        PositionIncrementAttribute postAtrr = stream.addAttribute(PositionIncrementAttribute.class);
 
        //오프셋위치확인
 
        OffsetAttribute offsetAttr = stream.addAttribute(OffsetAttribute.class);
 
        //텀타입 속성 확인
 
        TypeAttribute typeAttr = stream.addAttribute(TypeAttribute.class);
 
        
 
        //stream.incrementToken을 위해 필요
 
        stream.reset();
 
        
 
        int position = 0;
 
        
 
        while (stream.incrementToken()) {
 
            int increment = postAtrr.getPositionIncrement();
 
            
 
            position = position + increment;
 
            System.out.println();
 
            System.out.print(position + ": ");
 
            System.out.print("[ "+cattr.toString()+" : " + offsetAttr.startOffset()+"->"+offsetAttr.endOffset()+"                         : "+typeAttr.type()+" ]");
 
        }
 
 
 
        stream.end();
 
        stream.close();
 
        
 
    }
 
}
cs



-> 이 소스를 간단히 설명하면 커스텀한 분석기를 만들고 그 분석기를 이용해 분석된 사용자 입력 문장을 결과로 뿌려주는 역할을 하는 소스이다.

     1)사용자 정의 분석기

- 지금 작성한 커스텀 분석기는 StandardTokenizer에 SynonymFiler를 붙인 것이다. 여기서 빌더패턴을 이용하여 SynonymMap이란 객체를 다루고 있는데, 이것은 유사어 필터에게 유사어 목록이 담긴 맵을 전달해주기 위한 과정이다. 그리고 사용할 Tokenizer 클래스를 생성하고 사용할 필터의 input으로 토크나이저 객체를 전달해준다. 그리고 유사어 목록이 담긴 맵을 전달해주고, 원 단어를 저장할 것인가 안할 것인가를 지정하는 boolean타입의 매개변수까지 전달을 해준다. 그리고 마지막으로 TokenStreamComponents를 리턴해준다. 여기서 하나빼먹은 설명은 filter는 여러개가 될 수 있다는 점이다. 그래서 유사어 필터전 모든 텀을 소문자로 바꿔주는 LowerCaseFilter를 적용했다. 그런데 조금 설명이 필요한 점이라면 컴포지트 패턴을 이용하여 필터를 이어붙이고 있다는 점이다


   2)분석기 적용 결과

- 분석기의 tokenStream 메소드를 호출하면 최종적인 처리가된 TokenStream객체를 리턴해준다. 이 TokenStream 객체를 이용하여 분석된 결과를 출력할 수 있다.(최종적으로 색인에 들어가는 데이터는 TokenStream에 텀과 여러가지 메타데이터가 담기는 데이터이다.) 나머지 소스는 주석으로 충분히 예측가능할 것이다.






마지막 결과를 확인하면 JUMPS라는 단어가 소문자로 되어 유사어 필터가 적용되는 것을 볼 수 있다. 하지만 조금 특이한 점이 있다.


<결과>

Analyzing "나는 JUMPS 할거야"



  SynonymAnalyzer  

    

1: [ 나는 : 0->2 : <HANGUL> ]

2: [ jumps : 3->8 : <ALPHANUM> ]

2: [ 점프,뛰다 : 3->8 : SYNONYM ]

3: [ 할거야 : 9->12 : <HANGUL> ]


원단어와 유사어 처리된 단어가 위치 값이 같은 것이다. 즉, 색인에는 원단어는 물론 유사어까지 같은 포지션을 갖고 색인된다는 것이다. 이 말은 색인과정에서 유사어 필터를 등록한다면 검색에서는 유사어가 포함이 되어 있는 구문으로 구문검색을 해도 색인했던 원문 Document가 검색될 수 있다는 점이다. 아주 좋은 기능일 것 같다. 하지만 유사어 필터는 결코 가벼운 작업이 아니기에 꼭 색인 혹은 검색 둘중하나의 과정에만 적용시키면 된다. 보통 색인과 검색에 둘다 유사어 필터가 담긴 분석기를 사용하기도 하는데, 나중에 아주 데이터가 커지고 애플리케이션이 커지면 영향을 미칠 수도 있을 것같다.


posted by 여성게
:

Hashtable, HashMap, ConcurrentHashMap 비교


1. Hashtable, HashMap, ConcurrentHashMap

위에 나열된 클래스들은 Map 인터페이스를 구현한 콜렉션들입니다. 이 콜렉션들은 비슷한 역할을 하는것 같으면서도 다르게 구현되어 있습니다. 기본적으로 Map 인터페이스를 구축한다면 <key, value>구조를 가지게 됩니다. 하나씩 살펴봅시다.



2. Hashtable

Hashtable은 putget과 같은 주요 메소드에 synchronized 키워드가 선언 되어 있습니다. 또한 key, value에 null을 허용하지 않습니다. 

3. HashMap

HashMap은 주요 메소드에 synchronized 키워드가 없습니다. 또한 Hashtable과 다르게 key, value에 null을 입력할 수 있습니다.

하지만 HashMap도 

"Map<String,Integer> map = Collections.synchronizedMap(new HashMap<String,Integer>());"

와 같이 선언하면 Thread-safe한 맵으로 사용가능하다.

4. ConcurrentHashMap

HashMap을 thread-safe 하도록 만든 클래스가 ConcurrentHashMap입니다. 하지만 HashMap과는 다르게 key, value에 null을 허용하지 않습니다. 또한 putIfAbsent라는 메소드를 가지고 있습니다. 

5. Common Methods

위의 세종류의 클래스들은 putget 메소드 외에도 기본적인 메소드들을 구현하고 있습니다. 대표적인 몇가지의 메소드들만 알아봅시다.

  • clear()

    해당 콜렉션의 데이터를 초기화 합니다.

  • containsKey(key)

    해당 콜렉션에 입력 받은 key를 가지고 있는지 체크합니다.

  • containsValue(value)

    해당 콜렉션에 입력 받은 value를 가지고 있는지 체크합니다.

  • remove(key)

    해당 콜렉션에 입력 받은 key의 데이터(key도 포함)를 제거합니다.

  • isEmpty()

    해당 콜렉션이 비어 있는지 체크합니다.

  • size()

    해당 콜렉션의 엔트리(Entry) 또는 세그먼트(Segment) 사이즈를 반환합니다.



6. In Multi Threads(ConcurrentModificationException...)

HashMap에 대한 부분은 동기화가 이루어지지 않습니다. 하지만 HashMap을 쓰더라도 synchronized 블록을 선언해 주면 정상으로 동작을 합니다. 따라서 동기화 이슈가 있다면 일반적인 HashMap을 쓰지 말거나 쓰더라도 동기화를 보장하는 HashMap 콜렉션 또는 synchronized 키워드를 이용해 동기화 처리를 반드시 해주는 것이 좋아보입니다. 혹은 Thread-safe한 ConcurrentHashMap을 쓰시는 것을 권장합니다.

만약 하나의 스레드가 Map에 접근하여 요소들을 삭제,수정,삽입 등을 작업하고 있는 도중에 다른 스레드가 해당 Map에 접근 해 무엇인가를 작업한다면 동기화 문제가 발생할 수 있습니다.

밑의 소스에서 일반 HashMap을 사용한다면 위에서 말한 예외가 발생할 경우가 생깁니다. 이 경우를 ConcurrentHashMap을 사용해 Thread-safe한 코드로 변경하였습니다.


@Service

public class SessionService {

    private static final Logger log = LoggerFactory.getLogger(SessionService.class);

    /*Map<String, SessionInfo> sessionMap = new HashMap<>();*/

    /*

     * 

     * 설명 : HashMap을 썼을 경우, ConcurrentModificationException 발생(Thread간의 동기화문제)

     * HashMap -> ConcurrentHashMap

     */

    Map<String, SessionInfo> sessionMap = new ConcurrentHashMap<>();

    Boolean runFlag = true;

    private Thread itsThread = null;

    @Value("${app.session.expire.sec}")

    private int expiredSec;

     

    @PostConstruct

    private void dropExptiredSession() {

        itsThread = new Thread(() -> {

            try {

                while (runFlag) {

                /*

                 * yeoseong_yoon

                 * 설명 : 바로 밑에 메소드 주석 풀면 10초마다 스레드가 돌아가면서 세션만료시간이 된 

                 * 채팅세션을 remove한다.

                 */

                    checkExpiredSession();

                    Thread.sleep(10000);

                }

            } catch (InterruptedException e) {

                log.warn("thread interrupt occured", e);

            }

        });

        itsThread.start();

    }

    @PreDestroy

    private void stop() {

        runFlag = false;

        itsThread.interrupt();

    }

    

private void checkExpiredSession() {

for (Map.Entry entry : sessionMap.entrySet()) {

String sessionKey = (String) entry.getKey();

SessionInfo sessionInfo = (SessionInfo) entry.getValue();

long duration = TimeUtils.getCurrentSec() - sessionInfo.getLastTimeSec();

if (duration > expiredSec) {

sessionMap.remove(sessionKey);

log.info("session time out, sessionkey={}", sessionKey);

}

}

}

}




posted by 여성게
:
Search-Engine/Lucene 2019. 1. 29. 23:04

Lucene - 분석기(Analyzer)로 분석한 토큰(Token)결과 출력




루씬에서 색인을 하기위해서는 선행과정이 있다. 물론 문서안에 정의된 여러개의 필드에 적용한 속성에 따라 다르긴 하지만 ANALYZE속성을 적용한 필드인 경우에는 색인하기 이전에 텍스트를 토큰으로 추출하고 그 토큰에 여러가지 메타정보(start,end 정수/위치증가값 등등의 데이터)를 섞은 텀으로 만든 후에 색인에 들어간다. 여기에서 보여줄 예제는 색인을 위한 텍스트에 분석기의 분석과정을 적용 후에 어떻게 토큰이 분리되는지 확인하는 간단한 예제이다.


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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
package com.lucene.study;
 
 
 
import org.apache.lucene.analysis.core.SimpleAnalyzer;
 
import org.apache.lucene.analysis.core.StopAnalyzer;
 
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
 
import org.apache.lucene.analysis.standard.StandardAnalyzer;
 
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
 
import org.apache.lucene.analysis.tokenattributes.CharTermAttributeImpl;
 
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
 
import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
 
import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
 
import org.apache.lucene.util.AttributeImpl;
 
 
 
import java.io.IOException;
 
import java.io.StringReader;
 
 
 
 
 
import org.apache.lucene.analysis.*;
 
 
 
public class AnalyzerTest {
 
    
 
    
 
    private static final String[] examples = {"The quick brown fox jumped over the lazy dog","XY&Z Corporation - zyz@example.com","안녕하세요? 피자 주문하려고 하는데요."};
 
    
 
    private static final Analyzer[] analyzers = new Analyzer[] {new WhitespaceAnalyzer(),new SimpleAnalyzer(),new StopAnalyzer(),new StandardAnalyzer()};
 
    
 
    
 
    public static void analyze(String text) throws IOException {
 
        System.out.println("Analyzing \"" +text+ "\"");
 
        System.out.println("\n");
 
        for(Analyzer analyzer : analyzers) {
 
            
 
            String name = analyzer.getClass().getSimpleName();
 
            System.out.println("  "+name+"  ");
 
            System.out.print("    ");
 
            AnalyzerUtils.displayTokens(analyzer,text);
 
            
 
            System.out.println("\n");
 
        }
 
    }
 
    public static void main(String[] args) throws IOException {
 
        // TODO Auto-generated method stub
 
        String[] strings = examples;
 
        
 
        for(String text:strings) {
 
            analyze(text);
 
        }
 
    }
 
 
 
}
 
 
 
class AnalyzerUtils{
 
    public static void displayTokens(Analyzer analyzer,String text) throws IOException {
 
        displayTokens(analyzer.tokenStream("content"new StringReader(text)));
 
    }
 
    
 
    public static void displayTokens(TokenStream stream) throws IOException {
 
        
 
        //텀 속성확인
 
        CharTermAttribute cattr = stream.addAttribute(CharTermAttribute.class);
 
        
 
        //위치증가값 속성 확인
 
        PositionIncrementAttribute postAtrr = stream.addAttribute(PositionIncrementAttribute.class);
 
        //오프셋위치확인
 
        OffsetAttribute offsetAttr = stream.addAttribute(OffsetAttribute.class);
 
        //텀타입 속성 확인
 
        TypeAttribute typeAttr = stream.addAttribute(TypeAttribute.class);
 
        
 
        
 
        stream.reset();
 
        
 
        int position = 0;
 
        
 
        while (stream.incrementToken()) {
 
            int increment = postAtrr.getPositionIncrement();
 
            
 
            position = position + increment;
 
            System.out.println();
 
            System.out.print(position + ": ");
 
            System.out.print("[ "+cattr.toString()+" : " + offsetAttr.startOffset()+"->"+offsetAttr.endOffset()+" : "+typeAttr.type()+" ]");
 
        }
 
 
 
        stream.end();
 
        stream.close();
 
        
 
    }
 
}
cs



posted by 여성게
:

https://plposer.tistory.com/29

위의 포스트된 글 일부에 컴포지트 패턴이 설명되어있으며, 예제코드도 나와있음.

posted by 여성게
:
Web/JPA 2019. 1. 24. 13:02

JPA- 연관관계 외래키의 주인과 주인의 참조자 관계





JPA에서 일대일,일대다,다대일,다대다 관계에서는 항상 연관관계의 주인이 존재한다. 연관관계의 주인이라고 함은 데이터베이스 테이블에서 외래키의 주인을 뜻한다. 보통 일대다,다대일(다대다 포함, 다대다는 일대다,다대일관계로 매핑을 시킴) 관계에서는 보통 다쪽에 외래키가 존재한다. 일단 예제를 보면서 설명하겠다.






연관관계 예시



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
package com.spring.jpa.entitiy;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
 
@Entity
@Table(name = "TEAM_MEMBER")
public class TeamMember {
    
    @Id
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @Column(name = "MEMBER_NAME")
    private String memberName;
    
    //연관관계 매핑, 한팀에 여러 회원이 소속될 수 있음으로 다대일 관계이다.
    @ManyToOne
    //외래키로 사용될 컬럼의 이름이다.(디폴트는 해당 객체의 필드명+조인테이블의 칼럼명이다.)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getMemberName() {
        return memberName;
    }
 
    public void setMemberName(String memberName) {
        this.memberName = memberName;
    }
 
    public Team getTeam() {
        return team;
    }
    
    //연관관계에서는 객체관점에서도 똑같이 값을 세팅해주는 것이 좋다.
    //team.getMembers().add(this)는 데이터베이스에는 절대 영향을 미치지는 않지만
    //객체관점에서는 같이 set을 해주는 것이 맞다.
    public void setTeam(Team team) {
        this.team = team;
        
        if(!team.getMembers().contains(this)) {
            team.getMembers().add(this);
        }
    }
 
    @Override
    public String toString() {
        return "TeamMember [id=" + id + ", memberName=" + memberName + ", team=" + team.getName() + "]";
    }
    
}
 
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
package com.spring.jpa.entitiy;
 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
 
@Entity
@Table(name = "TB_TEAM")
public class Team {
    
    @Id
    @Column(name = "TEAM_ID")
    private Long id;
    
    
    @Column(name = "TEAM_NAME")
    private String name;
    
    //컬렉션의 제네릭타입이 존재할때
    //양방향 관계를 정의할때, mappedBy = "team" 여기서 team은 TeamMember테이블의
    //Team 엔티티의 필드 네임이다.(테이블 칼럼명이 아니다)
    @OneToMany(mappedBy = "team")
    private List<TeamMember> members = new ArrayList<TeamMember>();
    
    //컬렉션의 제네릭타입이 존재하지 않을때
    /*@OneToMany(targetEntity=TeamMember.class)
    private List members;*/
 
    public Long getId() {
        return id;
    }
 
 
    public void setId(Long id) {
        this.id = id;
    }
 
 
    public String getName() {
        return name;
    }
 
 
    public void setName(String name) {
        this.name = name;
    }
 
 
    public List<TeamMember> getMembers() {
        return members;
    }
 
 
    public void setMembers(List<TeamMember> members) {
        this.members = members;
    }
 
 
    @Override
    public String toString() {
        return "Team [id=" + id + ", name=" + name + ", members=" + Arrays.toString(members.toArray()) + "]";
    }
    
    
}
 
cs


데이터베이스에서는 양방향 참조가 가능하다.(외래키를 이용) 하지만 JPA에서는 사실 양방향이라는 것은 존재하지 않는다. 즉, 각각 다른 단방향으로 마치 양방인것처럼 꾸미는 것이다. 여기서 중요한 것은 연관관계의 주인인 TeamMember에서는 JPA로 조회를 할 경우 연관관계에 있는 Team 필드가 데이터로 채워진다. 그말은 즉, 객체 그래프탐색이 가능하다는 것이다. 하지만 mappedBy ="team"으로 어노테이션 속성이 달린 연관관계의 주인이 아닌 엔티티는 JPA로 조회를 해도 연관관계에 있는 엔티티리스트를 가져오지 않는다. 그렇기 때문에 연관관계의 주인인 엔티티의 setter에서 위와같은 로직이 포함되어야 한다. 그래야 연관관계의 주인이 아닌 엔티티에서도 객체 그래프 탐색이 가능하다. 왜냐하면 영속성 컨텍스트에서는 원래 엔티티의 복사본을 가지고 있기 때문에 원래 엔티티의 복사본과 1차캐시에서 가져간 엔티티를 비교하여 변경이 발생한다면 1차 캐시에 변경된 내용을 반영하여 주인이 아닌 엔티티에서도 해당 연관관계의 엔티티 데이터가 반영이 된다. 하지만 여기서 중요한 것은 setter로 엔티티를 변경하던가 위의 setter에 로직이 추가되어 List에 add가 되던 1차캐시의 복사본과 비교를 통해 변경을 반영하려면 반드시 트랜잭션 내에 존재해야한다는 것이다.

posted by 여성게
:
Web/JPA 2019. 1. 24. 11:58

JPA - 다대다 연관관계(@ManyToMany),N:N





설명에 앞서 사실 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 곤계를 표현할 수 없다. 그래서 보통 다대다 관계를 일대다,다대일 관계로 풀어내는 연결 테이블을 사용한다. 왜냐하면 다대다 관계를 1:1 테이블 매핑은 한다고 생각해보자. 회원과 상품의 관계인데, 한 회원이 여러개의 상품을 구입할 수 있고, 한 상품(ID)이 여러 회원에 의해 구입될 수 있다. 그렇다면 서로 몇개까지 살 수 있냐라는 제한이 없으면 외래키가 유동적으로 늘어난다. 그렇다면 엄청 많은 외래키를 굳이 미리 생성할 필요도 없다. 즉, 이렇게 몇개인지 알수 없는 다대다 관계를 중간에 연결 테이블 하나를 두고 일대다, 다대일 관계로 매핑을 시켜주는 것이다. 연결테이블은 단순히 하나의 로우에 회원의 기본키,상품의 기본키를 가지고 있으면 되므로 관계의 수가 늘어나면 단순히 로우의 수만 증가시켜주면 되기 때문이다.







(현재소스와는 관계없는 그림)



다대다(@ManyToMany) 매핑 방법 1



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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package com.spring.jpa.entitiy;
 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
 
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import javax.persistence.TableGenerator;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.UniqueConstraint;
 
import com.spring.jpa.common.RoleType;
 
 
/*
 * 유니크키 설정 및 nullable,length 등의 속성은 모두 auto DDL을 사용했을 때만 유효한 설정이다.
 * 즉, 테이블을 직접 생성한다면 적용되지 않는다. 하지만 테이블과 객체간의 관계표현에 있어 해당 설정들을 해놓으면
 * 엔티티 클래스만 봐도 테이블의 구조가 파악되기에 가독성을 위해서라도 설정을 해놓는 것이 좋다.
 */
/*
 * sequence table
 * CREATE TABLE MY_SEQUENCE(
 *    sequence_name varchar2(255) PRIMARY KEY,
 *    next_val number(22,0)
 * )
 */
@Entity
@Table(name = "MEMBER"
       ,uniqueConstraints = {
           @UniqueConstraint(
                   name = "NAME_AGE_UNIQUE",
                   columnNames = {"NAME","AGE"//uniqueConstraints는 auto DDL 속성을 사용할때만 유효한 설정이다.
           )
})
public class Member {
    
    @Id
    @Column(name = "MEMBER_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
    @TableGenerator(
            name="MEMBER_SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="MEMBER_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=50
    )
    private Long id;
    
    /*
     * not null
     * varchar2(10) -> 기본값 255;
     */
    @Column(name = "NAME",nullable=false,length=10)
    private String username;
    
    private Integer age;
    
    /*
     * EnumType의 기본값 설정은 정수이다.
     */
    @Enumerated(EnumType.STRING)
    @Column(name="ROLE_TYPE",nullable=false,length=20)
    private RoleType roleType;
    
    @Temporal(TemporalType.TIMESTAMP)
    @Access(AccessType.FIELD)
    private Date createdDate = new Date();
    
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
    
    @Lob
    private String description;
    
    @ManyToMany
    //다대다를 일대다-다대일 관계로 연결해줄 테이블명
    @JoinTable(name = "MEMBER_PRODUCT_CONN",
               joinColumns = @JoinColumn(name = "MEMBER_ID"),//멤버랑 연결시켜줄 연결테이블의 컬럼명(현재방향)
               inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))//상품과 연결시켜줄 연결테이블의 칼럼명(반대방향)
    private List<Product> products = new ArrayList<Product>();
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getUsername() {
        return username;
    }
 
    public void setUsername(String username) {
        this.username = username;
    }
 
    public Integer getAge() {
        return age;
    }
 
    public void setAge(Integer age) {
        this.age = age;
    }
 
    public RoleType getRoleType() {
        return roleType;
    }
 
    public void setRoleType(RoleType roleType) {
        this.roleType = roleType;
    }
 
    public Date getCreatedDate() {
        return createdDate;
    }
 
    public void setCreatedDate(Date createdDate) {
        this.createdDate = createdDate;
    }
 
    public Date getLastModifiedDate() {
        return lastModifiedDate;
    }
 
    public void setLastModifiedDate(Date lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }
 
    public String getDescription() {
        return description;
    }
 
    public void setDescription(String description) {
        this.description = description;
    }
 
    public List<Product> getProducts() {
        return products;
    }
 
    public void setProducts(List<Product> products) {
        this.products = products;
    }
 
    @Override
    public String toString() {
        return "Member [id=" + id + ", username=" + username + ", age=" + age + ", roleType=" + roleType
                + ", createdDate=" + createdDate + ", lastModifiedDate=" + lastModifiedDate + ", description="
                + description + ", products=" + Arrays.toString(products.toArray()) + "]";
    }
 
    
    
}
 
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
package com.spring.jpa.entitiy;
 
import java.util.ArrayList;
import java.util.List;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import javax.persistence.TableGenerator;
 
@Entity
@Table(name = "TB_PRODUCT")
public class Product {
    
    @Id
    @Column(name = "PRODUCT_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "PRODUCT_SEQ_GENERATOR")
    @TableGenerator(
            name="PRODUCT_SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="PRODUCT_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=50
    )
    private Long id;
    
    @Column(name = "PRODUCT_NAME")
    private String name;
    
    @ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<Member>();
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public List<Member> getMembers() {
        return members;
    }
 
    public void setMembers(List<Member> members) {
        this.members = members;
    }
    
    
}
 
cs



@ManyToMany
    //다대다를 일대다-다대일 관계로 연결해줄 테이블명
    @JoinTable(name = "MEMBER_PRODUCT_CONN",
               joinColumns = @JoinColumn(name = "MEMBER_ID"),//멤버랑 연결시켜줄 연결테이블의 컬럼명(현재방향)
               inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))//상품과 연결시켜줄 연결테이블의 칼럼명(반대방향)
    private List<Product> products = new ArrayList<Product>();



매핑에 대한 상세 설명을 하자면 우선 연관관계를 맺을 필드에 @ManyToMany 어노테이션을 달아준다. 이것은 현재 참조하고 있는 컬렉션과 다대다 관계임을 명시해준다. 하지만 여기서 의문이 드는것이 "다대다 관계가 안된다며?"이다. 이것은 다음 속성에 나온다. @JoinTable로 중간에 연결테이블에 대한 속성을 정의해준다. name속성은 연결테이블의 이름, joinColums는 연관관계를 맺어줄 연결테이블의 컬럼을 정의해준다. @JoinColums는 현재 회원테이블에 대한 외래키이고, inverseJoinColums는 반대쪽 상품 테이블에 대한 외래키이다. 그리고 반드시 연관관계에는 연관관계의 주인이 있어야 하므로, 회원테이블에 연관관계의 주인임을 명시해주었다.(상품에 mappedBy속성이 있음으로) 즉, @ManyToMany도 결국에 데이터베이스에는 일대다,다대일 관계로 매핑되며 중간에 연결테이블이 생성된다.





이렇게 편하게 다대다 관계를 맺어줄 수 있다. 하지만 이 연관관계는 한가지 한계점이 존재한다. 실무에서는 연결테이블에 단순 외래키만 존재하길 원하지 않는다. 회원이 몇개의 상품을 주문했는지의 수량, 언제 주문했는지 날짜등의 데이터를 연결테이블에 있길 원할 수도 있기 때문인데, 이것은 @ManyToMany로 매핑할 수 없다. 이 한계점을 개선한 다대다 매핑을 다음에 설명한다.





다대다(@ManyToMany -> @OneToMany,@ManyToOne & 복합기본키 ) 매핑 방법 2




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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package com.spring.jpa.manytomanyexpend;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.TableGenerator;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.UniqueConstraint;
 
import com.spring.jpa.common.RoleType;
import com.spring.jpa.entitiy.Product;
 
@Entity
@Table(name = "MEMBER_2"
       /*,uniqueConstraints = {
           @UniqueConstraint(
                   name = "NAME_AGE_UNIQUE",
                   columnNames = {"NAME","AGE"} //uniqueConstraints는 auto DDL 속성을 사용할때만 유효한 설정이다.
           )
}*/)
public class Member_2 {
    
    @Id
    @Column(name = "MEMBER_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "MEMBER2_SEQ_GENERATOR")
    @TableGenerator(
            name="MEMBER2_SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="MEMBER2_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=50
    )
    private Long id;
    
    /*
     * not null
     * varchar2(10) -> 기본값 255;
     */
    @Column(name = "NAME",/*nullable=false,*/length=10)
    private String username;
    
    private Integer age;
    
    /*
     * EnumType의 기본값 설정은 정수이다.
     */
    @Enumerated(EnumType.STRING)
    @Column(name="ROLE_TYPE",/*nullable=false,*/length=20)
    private RoleType roleType;
    
    @Temporal(TemporalType.TIMESTAMP)
    @Access(AccessType.FIELD)
    private Date createdDate = new Date();
    
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
    
    @Lob
    private String description;
    
    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts ;
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getUsername() {
        return username;
    }
 
    public void setUsername(String username) {
        this.username = username;
    }
 
    public Integer getAge() {
        return age;
    }
 
    public void setAge(Integer age) {
        this.age = age;
    }
 
    public RoleType getRoleType() {
        return roleType;
    }
 
    public void setRoleType(RoleType roleType) {
        this.roleType = roleType;
    }
 
    public Date getCreatedDate() {
        return createdDate;
    }
 
    public void setCreatedDate(Date createdDate) {
        this.createdDate = createdDate;
    }
 
    public Date getLastModifiedDate() {
        return lastModifiedDate;
    }
 
    public void setLastModifiedDate(Date lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }
 
    public String getDescription() {
        return description;
    }
 
    public void setDescription(String description) {
        this.description = description;
    }
 
    public List<MemberProduct> getMemberProducts() {
        return memberProducts;
    }
 
    public void setMemberProducts(List<MemberProduct> memberProducts) {
        this.memberProducts = memberProducts;
    }
 
    
    
}
 
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
package com.spring.jpa.manytomanyexpend;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.TableGenerator;
 
//회원엔티티와 상품엔티티의 다대다 연결을 위한 연결엔티티이다.
@Entity
//복합 기본키 매핑을 위한 식별자 클래스
@IdClass(MemberProductId.class)
public class MemberProduct {
    
    @Id
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID"//외래키의 주인이며, MEMBER_ID로 해당테이블에 외래키가 생성된다.
    private Member_2 member;
    
    @Id
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID"//위와 동일
    private Product2 product;
    
    private int orderAmount;
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public Member_2 getMember() {
        return member;
    }
 
    public void setMember(Member_2 member) {
        this.member = member;
    }
 
    public Product2 getProduct() {
        return product;
    }
 
    public void setProduct(Product2 product) {
        this.product = product;
    }
 
    public int getOrderAmount() {
        return orderAmount;
    }
 
    public void setOrderAmount(int orderAmount) {
        this.orderAmount = orderAmount;
    }
    
    
}
 
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
package com.spring.jpa.manytomanyexpend;
 
import java.util.ArrayList;
import java.util.List;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import javax.persistence.TableGenerator;
 
import com.spring.jpa.entitiy.Member;
 
@Entity
@Table(name = "TB_PRODUCT2")
public class Product2 {
    
    @Id
    @Column(name = "PRODUCT_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "PRODUCT2_SEQ_GENERATOR")
    @TableGenerator(
            name="PRODUCT2_SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="PRODUCT2_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=50
    )
    private Long id;
    
    @Column(name = "PRODUCT_NAME")
    private String name;
    
    /*@ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<Member>();*/
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
    
    
}
 
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
package com.spring.jpa.manytomanyexpend;
 
import java.io.Serializable;
 
public class MemberProductId implements Serializable{
    
    private Long member;
    private Long product;
    
    
    public Long getMember() {
        return member;
    }
    public void setMember(Long member) {
        this.member = member;
    }
    public Long getProduct() {
        return product;
    }
    public void setProduct(Long product) {
        this.product = product;
    }
    
    //복합키 식별자 클래스는 반드시 밑의 메소드를 오버라이드해주어야한다.
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((member == null) ? 0 : member.hashCode());
        result = prime * result + ((product == null) ? 0 : product.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        MemberProductId other = (MemberProductId) obj;
        if (member == null) {
            if (other.member != null)
                return false;
        } else if (!member.equals(other.member))
            return false;
        if (product == null) {
            if (other.product != null)
                return false;
        } else if (!product.equals(other.product))
            return false;
        return true;
    }
    
}
 
cs



이번 다대다 매핑은 명시적으로 어노테이션도 @OneToMany,@ManyToOne으로 연관관계를 맺었다. 이전에는 내부적으로 연결테이블을 생성했지만 지금은 명시적으로 연결 엔티티를 생성해준다. 그리고 연결엔티티에서 @ManyToOne 어노테이션을 갖는다. 이 말은 즉슨, 연결테이블이 연관관계의 주인이 되는 것이다.(보통 데이터베이스에서 다대일,일대다 관계에서 다 쪽에 외래키를 갖는다.) 하지만 여기서 조금 특이한 것이 있다면 @IdClass이다. 이것은 회원과 상품의 기본키를 연결테이블에서 외래키로 사용함과 동시에 두키를 복합키로 하여 기본키를 지정하기 때문에 식별자 클래스가 추가된 것이다.(회원의 외래키와 상품의 외래키를 복합키로 하여 기본키로 지정) 식별자 클래스는 별개 없다. 단순히 각 테이블(회원,상품)의 기본키의 필드타입으로 하여서 두개의 식별자로 사용될 필드를 선언하고 Getter,Setter메소드를 만들어 준 후에 IDE의 기능을 이용하여 hashCode()와 equals()를 자동 구현해주면된다. 그리고 마지막으로 @Embeddable 클래스는 반드시 기본생성자를 필수로 생성해주고, Serializable을 implements 해주면 된다. 하지만 복합키도 좋지만 이렇게 되면 해야할 일이 늘어난다. 식별자 클래스를 만들어주는 등의.... 그래서 다음 매핑방법에는 복합키를 사용하지 않고 연결엔티티에 별도로 기본키를 할당해주어서 식별자 클래스등을 만드는 불편함을 줄이겠다.






다대다(@ManyToMany -> @OneToMany,@ManyToOne ) 매핑 방법 3



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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package com.spring.jpa.manytomanyexpend;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.TableGenerator;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.UniqueConstraint;
 
import com.spring.jpa.common.RoleType;
import com.spring.jpa.entitiy.Product;
 
@Entity
@Table(name = "MEMBER_2"
       /*,uniqueConstraints = {
           @UniqueConstraint(
                   name = "NAME_AGE_UNIQUE",
                   columnNames = {"NAME","AGE"} //uniqueConstraints는 auto DDL 속성을 사용할때만 유효한 설정이다.
           )
}*/)
public class Member_2 {
    
    @Id
    @Column(name = "MEMBER_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "MEMBER2_SEQ_GENERATOR")
    @TableGenerator(
            name="MEMBER2_SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="MEMBER2_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=50
    )
    private Long id;
    
    /*
     * not null
     * varchar2(10) -> 기본값 255;
     */
    @Column(name = "NAME",/*nullable=false,*/length=10)
    private String username;
    
    private Integer age;
    
    /*
     * EnumType의 기본값 설정은 정수이다.
     */
    @Enumerated(EnumType.STRING)
    @Column(name="ROLE_TYPE",/*nullable=false,*/length=20)
    private RoleType roleType;
    
    @Temporal(TemporalType.TIMESTAMP)
    @Access(AccessType.FIELD)
    private Date createdDate = new Date();
    
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
    
    @Lob
    private String description;
    
    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<MemberProduct>();
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getUsername() {
        return username;
    }
 
    public void setUsername(String username) {
        this.username = username;
    }
 
    public Integer getAge() {
        return age;
    }
 
    public void setAge(Integer age) {
        this.age = age;
    }
 
    public RoleType getRoleType() {
        return roleType;
    }
 
    public void setRoleType(RoleType roleType) {
        this.roleType = roleType;
    }
 
    public Date getCreatedDate() {
        return createdDate;
    }
 
    public void setCreatedDate(Date createdDate) {
        this.createdDate = createdDate;
    }
 
    public Date getLastModifiedDate() {
        return lastModifiedDate;
    }
 
    public void setLastModifiedDate(Date lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }
 
    public String getDescription() {
        return description;
    }
 
    public void setDescription(String description) {
        this.description = description;
    }
 
    public List<MemberProduct> getMemberProducts() {
        return memberProducts;
    }
 
    public void setMemberProducts(List<MemberProduct> memberProducts) {
        this.memberProducts = memberProducts;
    }
 
    
    
}
 
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
package com.spring.jpa.manytomanyexpend;
 
import java.util.ArrayList;
import java.util.List;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.TableGenerator;
 
import com.spring.jpa.entitiy.Member;
 
@Entity
@Table(name = "TB_PRODUCT2")
public class Product2 {
    
    @Id
    @Column(name = "PRODUCT_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "PRODUCT2_SEQ_GENERATOR")
    @TableGenerator(
            name="PRODUCT2_SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="PRODUCT2_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=50
    )
    private Long id;
    
    @Column(name = "PRODUCT_NAME")
    private String name;
    
    @OneToMany(mappedBy = "product")
    private List<MemberProduct> members = new ArrayList<MemberProduct>();
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public List<MemberProduct> getMembers() {
        return members;
    }
 
    public void setMembers(List<MemberProduct> members) {
        this.members = members;
    }
    
    
}
 
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
package com.spring.jpa.manytomanyexpend;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.TableGenerator;
 
//회원엔티티와 상품엔티티의 다대다 연결을 위한 연결엔티티이다.
@Entity
//복합 기본키 매핑을 위한 식별자 클래스
/*@IdClass(MemberProductId.class)*/
public class MemberProduct {
    
    @Id
    @Column(name = "MEMBERPRODUCT_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "MEMBERPRODUCT_SEQ_GENERATOR")
    @TableGenerator(
            name="MEMBERPRODUCT_SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="MEMBERPRODUCT_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=50
    )
    private Long id;
    
    /*@Id*/
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID"//외래키의 주인이며, MEMBER_ID로 해당테이블에 외래키가 생성된다.
    private Member_2 member;
    
    /*@Id*/
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product2 product;
    
    private int orderAmount;
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public Member_2 getMember() {
        return member;
    }
 
    public void setMember(Member_2 member) {
        this.member = member;
    }
 
    public Product2 getProduct() {
        return product;
    }
 
    public void setProduct(Product2 product) {
        this.product = product;
    }
 
    public int getOrderAmount() {
        return orderAmount;
    }
 
    public void setOrderAmount(int orderAmount) {
        this.orderAmount = orderAmount;
    }
    
    
}
 
cs



크게 바뀐 것은 없다. 식별자 클래스가 없어지고, 연결엔티티에 외래키를 기본키로 사용하는 @Id가 없어지는 등 조금의 수정이 이루어졌을 뿐이다. 단순히 시퀀스를 기본키로 사용하므로서 복합키 매핑등의 조금은 복잡한 과정이 빠져 더 쉽게 연관관계 매핑을 할 수 있다. 위의 코드와 비교해보면 차이점을 금방 알 수 있다.

posted by 여성게
:
Web/JPA 2019. 1. 17. 22:51

JPA 기본 키 매핑 전략,@Id


JPA에서 기본 키 매핑 전략에는 크게 4가지가 있다.


1)직접 할당 : 기본 키를 애플리케이션에서 직접 엔티티클래스의 @Id 필드에 set해준다.

2)자동 생성 : 대리 키 사용 방식

- IDENTITY : 기본 키 생성을 데이터베이스에 위임한다.(ex MySQL - AUTO INCREMENT...)

- SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.(ex Oracle sequence...)

- TABLE : 키 생성 테이블을 사용한다.(ex 시퀀스용 테이블을 생성해서 테이블의 기본키를 저장하고 관리한다.)


자동 생성 전략이 이렇게 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문이다. 위 중에서 IDENTITY와 SEQUENCE는 데이터베이스 벤더에 의존적이다. 하지만 TABLE 전략은 키 생성용 테이블을 하나 만들어두고 마치 시퀀스처럼 사용하는 방법이기에 벤더에 의존하지 않는다.(하지만 각각 장단점이 존재함)


*키 생성 전략을 사용하려면 persistence.xml 혹은 application.properties(이것은 구글링 해봐야 할 듯)에 hibernate.id.new_generator_mappings = true 설정을 해주어야한다.






IDENTITY 전략


IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 전략이다. 주로 MySQL,PostgreSQL,SQL Server,DB2에서 사용한다


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
/*
 * 유니크키 설정 및 nullable,length 등의 속성은 모두 auto DDL을 사용했을 때만 유효한 설정이다.
 * 즉, 테이블을 직접 생성한다면 적용되지 않는다. 하지만 테이블과 객체간의 관계표현에 있어 해당 설정들을 해놓으면
 * 엔티티 클래스만 봐도 테이블의 구조가 파악되기에 가독성을 위해서라도 설정을 해놓는 것이 좋다.
 */
@Entity
@Table(name = "MEMBER"
        ,uniqueConstraints = {
        @UniqueConstraint(
                name = "NAME_AGE_UNIQUE",
                columnNames = {"NAME","AGE"//uniqueConstraints는 auto DDL 속성을 사용할때만 유효한 설정이다.
        )
})
@Getter
@Setter
@ToString
public class Member {
    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
 
    /*
     * not null
     * varchar2(10) -> 기본값 255;
     */
    @Column(name = "NAME",nullable=false,length=10)
    private String username;
 
    private Integer age;
 
    /*
     * EnumType의 기본값 설정은 정수이다.
     */
    @Enumerated(EnumType.STRING)
    @Column(name="ROLE_TYPE",nullable=false,length=20)
    private RoleType roleType;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
 
    @Lob
    private String description;
}
 
cs


IDENTITY 전략의 단점이라고 있는 특징이 있다. 그것은 데이터베이스에 값을 저장하고 나서야 기본 값을 구할 있다. 여기서 기본  

값을 구하는 것이 무슨 상관이냐? 라고 말할 있지만 영속성 컨텍스트에 1차캐시(엔티티의 영속상태화) 하기 위해서는 구분자로 기본키(@Id) 필드를 이용한다. , 영속성

컨텍스트에 캐싱을 하기위한 primary key 값을 가져오기 위하여 테이블을 추가로 조회하게 된다. 그래서 다른 전략과는 다른 행동중 하나가 persist()호출을

하자마자 지연쓰기를 하는 것이 아니라, primary key값을 가져오기 위하여 바로 flush 호출하게 된다.







SEQUENCE 전략




데이터베이스의 시퀀스 오브젝트를 이용하여 유일한 값을 순서대로 생성한다. 전략은 주로 Oracle,PostgreSQL,DB2,H2 등의 데이터베이스에서 사용할 있다.


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
/*
 * 유니크키 설정 및 nullable,length 등의 속성은 모두 auto DDL을 사용했을 때만 유효한 설정이다.
 * 즉, 테이블을 직접 생성한다면 적용되지 않는다. 하지만 테이블과 객체간의 관계표현에 있어 해당 설정들을 해놓으면
 * 엔티티 클래스만 봐도 테이블의 구조가 파악되기에 가독성을 위해서라도 설정을 해놓는 것이 좋다.
 */
/*
 * sequence table
 * CREATE TABLE MY_SEQUENCE(
 *    sequence_name varchar2(255) PRIMARY KEY,
 *    next_val number(22,0)
 * )
 */
@Entity
@SequenceGenerator(
        name="BOARD_SEQ_GENERATOR",
        sequenceName="BOARD_SEQ",
        initialValue=1,allocationsSize=1
)
@Table(name = "MEMBER"
        ,uniqueConstraints = {
        @UniqueConstraint(
                name = "NAME_AGE_UNIQUE",
                columnNames = {"NAME","AGE"//uniqueConstraints는 auto DDL 속성을 사용할때만 유효한 설정이다.
        )
})
@Getter
@Setter
@ToString
public class Member {
    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy=GenerationType.SEQUENCE
            ,generator="BOARD_SEQ_GENERATOR"
    )
    private Long id;
 
    /*
     * not null
     * varchar2(10) -> 기본값 255;
     */
    @Column(name = "NAME",nullable=false,length=10)
    private String username;
 
    private Integer age;
 
    /*
     * EnumType의 기본값 설정은 정수이다.
     */
    @Enumerated(EnumType.STRING)
    @Column(name="ROLE_TYPE",nullable=false,length=20)
    private RoleType roleType;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
 
    @Lob
    private String description;
}
cs




IDENTITY와는 다른 설정이 있다면 시퀀스생성기 어노테이션이다. 여기서 name속성은 실제 @Id필드에서 참조할 이름이라고 생각하면 되고, sequenceName 

실제 데이터베이스에 생성되는 시퀀스 오브젝트 이름이다. 그리고 시퀀스 초기값과 allocationSize라는 속성이 있다. 여기서 allocationSize 실제 데이터베이스에서

가져오는 시퀀스의 한번 호출에 증가하는 값의 크기이다. 이것은 성능 최적화와 관련된 속성이므로 마지막에 따로 설명한다.

SEQUENCE 전략은 em.persist() 호출할 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다.(실제 엔티티에 할당할 primary key) 그리고 조회한 식별자를

엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다. 이후 커밋이 일어나게 되면 실제 데이터베이스에 INSERT되게 된다.








TABLE 전략




TABLE 전략은 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다. 이것은 벤더에 의존적이지 않은 전략이다.

전략을 사용할 auto DDL설정을 했다면 상관없지만 나중에 프로덕환경에서의 데이터베이스 설계에서 시퀀스 테이블에 생성이 선행되어야한다.


CREATE TABLE APP_SEQUENCE(

sequence_name varchar2(255) primary key,

next_val number(22,0)

)


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
/*
 * 유니크키 설정 및 nullable,length 등의 속성은 모두 auto DDL을 사용했을 때만 유효한 설정이다.
 * 즉, 테이블을 직접 생성한다면 적용되지 않는다. 하지만 테이블과 객체간의 관계표현에 있어 해당 설정들을 해놓으면
 * 엔티티 클래스만 봐도 테이블의 구조가 파악되기에 가독성을 위해서라도 설정을 해놓는 것이 좋다.
 */
/*
 * sequence table
 * CREATE TABLE MY_SEQUENCE(
 *    sequence_name varchar2(255) PRIMARY KEY,
 *    next_val number(22,0)
 * )
 */
@Entity
@Table(name = "MEMBER"
        ,uniqueConstraints = {
        @UniqueConstraint(
                name = "NAME_AGE_UNIQUE",
                columnNames = {"NAME","AGE"//uniqueConstraints는 auto DDL 속성을 사용할때만 유효한 설정이다.
        )
})
public class Member {
    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
    @TableGenerator(
            name="MEMBER_SEQ_GENERATOR",
            table="MY_SEQUENCE"//시퀀스 생성용 테이블 이름
            pkColumnName="sequence_name"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="MEMBER_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=50
    )
    private Long id;
 
    /*
     * varchar2(10) -> 기본값 255;
     */
    @Column(name = "NAME",nullable=false,length=10)
    private String username;
 
    private Integer age;
 
    /*
     * EnumType의 기본값 설정은 정수이다.
     */
    @Enumerated(EnumType.STRING)
    @Column(name="ROLE_TYPE",nullable=false,length=20)
    private RoleType roleType;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
 
    @Lob
    private String description;
}
 
 
cs




MEMBER_SEQ_GENERATOR라는 테이블 생성기를 등록하였다. 여기의 설정들은 모두 주석으로 설명을 걸어놓았다






allocationSize 이용한 성능 최적화?





영속성 컨텍스트에 엔티티를 저장하기 위해 식별자를 구하는 과정을 설명하면,


1)식별자를 구하려고 데이터베이스 시퀀스를 조회한다.

->select board_seq.nextval from dual


그렇다면 만약 increment by 1이라는 설정이라면? 엔티티를 하나하나 저장할 때마다 데이터베이스에 엑세스하여 시퀀스 값을 가져와야한다.(allocationSize=1일때)


여기에서 allocationSize 이용하여 성능 최적화를 있다. allocationSize값을 적절히 크기를 키워 설정한 값만큼 번에 시퀀스 값을 증가시키고 나서

그만큼 메모리에서 기억해 시퀀스 자체를 메모리에서 할당하는 것이다. 예를 들어 allocationSize 값이 50이면 시퀀스를 한번 가져올때 마다 한번에 50 증가된

값을 받아온다. 그말은 처음 시퀀스 (50) 받아오면 1~50까지는 메모리에서 엔티티에 식별자를 할당한다. 그리고 51 되면 시퀀스 (100) 한번더 가져와 

51~100까지를 메모리에서 다시 할당해준다. 이말은 쉽게 말하자면 allocationSize=1 때보다 시퀀스 값을 가져오기 위해 데이터베이스에 엑세스하는 횟수를 49번을

줄인 것이다. insert 성능이 크게 중요하지 않은 애플리케이션은 상관없지만 반대는 성능최적화 전략을 사용해보는 것도 좋은 방안일 듯하다. 하지만 제대로

사용하지 못하면 아무리 좋은 것도 독이 되니 설계해서 쓰는 것이 좋을 같다.








기타





사실은 전략중에 AUTO전략이라는 것도 있다. 이것은 데이터베이스 벤더에 따라 자동으로 위의 3가지방법중 하나를 선택해 준다. 장점은 데이터베이스 벤더가 바뀌어도

코드에 수정이 없다는 것이다. 하지만 중요한 것은 사상이다. JPA 엔티티 클래스만 봐도 테이블이 예상이 되어야하는데 AUTO 가독성이 많이 떨어지기에

크게 추천하지 않고, 위의 3가지 방법중 하나를 선택하여 명시적으로 설정해주는 것이 좋을 같다.(개인적인 생각)

posted by 여성게
: