Web/Spring 2019. 6. 15. 21:57

 

오늘 포스팅할 내용은 개발 단계에서 없어서는 안되는 로그에 관한 포스팅입니다. 매번 sysout으로 로그를 찍는 것은 바보 같이 리소스를 낭비하는 짓이라는 것은 누구나 다 알고 계실겁니다. 저는 별소 lombok을 즐겨 사용하기에 @Slfj4 어노테이션을 자주 사용하게 됩니다. 하지만 이번에는 단순 콘솔에서만 출력하는 것이 아니라, 일자별 로그파일을 떨구어주는 설정파일에 대해 알아볼 것입니다. 바로 예제로 들어가겠습니다.(로그사용법에 대해서는 다루지 않습니다.)

 

참고로 Logback은 slf4j의 구현체이자 스프링 부트의 기본 로그 객체입니다. 기본으로 classpath에서 스프링부트가 읽어가는 파일 이름은 logback-spring.xml입니다. 물론 설정 파일명을 변경하실 수도 있습니다.

 

1
2
#logging config
logging.config=classpath:logging-config.xml
cs

 

저는 기본 설정파일명을 사용하지 않을 것이기 때문에 application.properties에 로깅 설정 파일명을 명시해줍니다.

 

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
<configuration>
    <!-- Created By yeoseong_yoon -->
    <!-- 로그 경로 변수 선언 -->
    <property name="LOG_DIR" value="${user.home}/logs/app" />
    <property name="LOG_PATH" value="${LOG_DIR}/app.log"/>
    
    <!-- Console Appender -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                %-5level %d{HH:mm:ss.SSS} [%thread %F:%L] %method - %msg%n
            </pattern>
        </encoder>
    </appender>
    <!-- Rolling File Appender -->
    <appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 파일 경로 -->
        <file>${LOG_PATH}</file>
        <!-- 출력패턴 -->
        <encoder>
            <pattern>%-5level %d{HH:mm:ss.SSS} [%thread %F:%L] %method - %msg%n</pattern>
        </encoder>
        <!-- Rolling 정책 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- .gz,.zip 등을 넣으면 자동으로 일자별 로그파일 압축 -->
            <fileNamePattern>${LOG_DIR}/app_%d{yyyy-MM-dd}_%i.log.gz</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- 파일당 최고 용량 10MB -->
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!-- 일자별 로그파일 최대 보관주기(일단위) 
            만약 해당 설정일 이상된 파일은 자동으로 제거-->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
    </appender>
 
 
    <logger name="애플리케이션패키지명(ex com.web.spring)" level="INFO"/>
 
    <root level="INFO"> <!-- DEBUG -->
        <appender-ref ref="ROLLING_FILE"/>
    </root>
</configuration>
cs

 

우선은 <property>를 이용하여 전역으로 사용할 변수명을 지정해줍니다. 이 설정파일에서는 로그파일의 경로, 로그파일 Full Path를 변수에 할당하였습니다. 그다음 중요한 설정인 <appender> 설정입니다. 로그 파일의 출력 형식을 지정해주는 설정이라고 보시면 됩니다. 사실 설정의 핵심중 하나입니다. 현재 설정에서는 console,rollingfile appender를 지정해주었습니다. 

 

Appender 종류

이름 설명
ConsoleAppender 콘솔에 로그를 찍음
FileAppender 파일에 로그를 찍음(하나의 파일에 로그를 계속 누적해서 찍음)
RollingFileAppender 여러개의 파일을 순회하면서 로그를 찍음
SMTPAppender 로그를 메일에 찍어 보냄
DBAppender DB에 로그를 찍음
기타 SocketAppender, SSLSocketAppender...  

 

패턴에 사용되는 요소

 

  • 1. %Logger{length}
  • - Logger name을 축약할 수 있다. {length}는 최대 자리 수
  • 2. %thread
  • - 현재 Thread 이름
  • 3. %-5level
  • - 로그 레벨, -5는 출력의 고정폭 값
  • 4. %msg
  • - 로그 메시지 (=%message)
  • 5. %n
  • - new line
  • 6. ${PID:-}
  • - 프로세스 아이디
  • 기타
  • %d : 로그 기록시간
  • %p : 로깅 레벨
  • %F : 로깅이 발생한 프로그램 파일명
  • %M : 로깅일 발생한 메소드의 이름
  • %l : 로깅이 발생한 호출지의 정보
  • %L : 로깅이 발생한 호출지의 라인 수
  • %t : 쓰레드 명
  • %c : 로깅이 발생한 카테고리
  • %C : 로깅이 발생한 클래스 명
  • %m : 로그 메시지
  • %r : 애플리케이션 시작 이후부터 로깅이 발생한 시점까지의 시간

 

위에 설정에서 하나 짚고 넘어갈 것이 있다면 RollingAppender의 <file>과 <fileNamePattern>이다. 일자별 로그파일을 찍기 위해 버퍼가 되는 파일이고 후자는 일자별 로그파일의 이름 패턴을 넣어주는 것이다.

 

이상 정말 간단한 Logback 설정방법을 다루어보았다. 이것보다 많은 설정들이 있을 것이다. 하지만 필자로 깊게 알지못하기에 간단한 사용법만 다루었다. 하지만 하나 중요한 사실은 로그를 남기는 것도 좋지만, 적정 레벨의 선택 그리고 포맷이다. 필자가 프로젝트를 하며 ELK를 이용하여 로그를 중앙 집중식으로 관리를 하게되었는데, 정규식패턴식과 같은 것으로 로그파일을 필터링 할 상황이 생겼다. 만약 구조화된 로그 출력이라면 정규식으로 필터링 걸기가 편할 것이지만 구조화되지 않고 애플리케이션마다 출력하는 패턴이 다르다면 필터링 하는 작업도 쉽지 않을 것이다.

posted by 여성게
:
Web/Spring 2019. 6. 13. 22:30

 

오늘 포스팅할 내용은 Spring의 RestTemplate입니다. 우선 RestTemplate란 Spring 3.0부터 지원하는 Back End 단에서 Http 통신에 유용하게 쓰이는 템플릿 객체이며, 복잡한 HttpClient 사용을 한번 추상화하여 Http 통신사용을 단순화한 객체입니다. 즉, HttpClient의 사용에 있어 기계적이고 반복적인 코드들을 한번 랩핑해서 손쉽게 사용할 수 있게 해줍니다. 또한 json,xml 포멧의 데이터를 RestTemplate이 직접 객체에 컨버팅해주기도 합니다.

 

이렇게 사용하기 편한 RestTemplate에서도 하나 짚고 넘어가야할 점이 있습니다. RestTemplate 같은 경우에는 Connection Pooling을 직접적으로 지원하지 않기 때문에 매번 RestTemplate를 호출할때마다, 로컬에서 임시 TCP 소켓을 개방하여 사용합니다. 또한 이렇게 사용된 TCP 소켓은 TIME_WAIT 상태가 되는데, 요청량이 엄청 나게 많아진다면 이러한 상태의 소켓들은 재사용 될 수 없기 때문에 응답이 지연이 될것입니다. 하지만 이러한 RestTemplate도 Connection Pooling을 이용할 수 있는데 이것은 바로 RestTemplate 내부 구성에 의해 가능합니다. 바로 내부적으로 사용되는 HttpClient를 이용하는 것입니다. 바로 예제 코드로 들어가겠습니다.

 

우선 Connection pool을 적용하기 위한 HttpClientBuilder를 사용하기 위해서는 dependency 라이브러리가 필요하다.

 

compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.6'

 

각자 필요한 버전을 명시해서 의존성을 추가해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    /*
     * Connection Pooling을 적용한 RestTemplate
     */
    @Bean(name="restTemplateClient")
    public RestTemplate restClient() {
        HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        /*
         * 타임아웃 설정
         */
        //httpRequestFactory.setConnectTimeout(timeout);
        //httpRequestFactory.setReadTimeout(timeout);
        //httpRequestFactory.setConnectionRequestTimeout(connectionRequestTimeout);
        HttpClient httpClient = HttpClientBuilder.create()
                                                 .setMaxConnTotal(150)
                                                 .setMaxConnPerRoute(50)
                                                 .build();
        httpRequestFactory.setHttpClient(httpClient);
        
        return new RestTemplate(httpRequestFactory);
    }
cs

 

위에 코드를 보면 최대 커넥션 수(MaxConnTotal)를 제한하고 IP,포트 1쌍 당 동시 수행할 연결 수(MaxConnPerRoute)를 제한하는 설정이 포함되어있습니다. 이런식으로 최대 커넥션 수를 150개로 제한하여 150개의 자원내에서 모든 일을 수행하게 되는 것입니다. 마치 DB의 Connection Pool 과 비슷한 역할이라고 보시면 됩니다.(물론 같지는 않음.) 그리고 RestTemplate은 Multi Thread 환경에서 safety 하기 때문에 빈으로 등록하여 가져다 쓰도록 하였습니다. 

 

마지막으로 구글링을 하던 도중에 Keep-alive 활성화가 되야지만 HttpClient의 Connection Pooling 지원이 가능하다고 나와있습니다. 기본적으로 HTTP1.1은 Keep-alive가 활성화되어 있지만 이부분은 더 깊게 알아봐야할 점인것 같습니다. 만약 해당 부분에 대해 아시는 분은 꼭 댓글에 코멘트 부탁드리겠습니다.

posted by 여성게
:
Web/Spring 2019. 5. 30. 10:32

오늘 포스팅할 내용은 웹프로그래밍에서 아주 자주 쓰이는 내용입니다. 바로 JSON->Object 혹은 Object->JSON 컨버팅하는 라이브러리 소개입니다. 우선은 스프링에서는 기본적으로 ObjectMapper라는 라이브러리를 사용하여 컨버팅 작업을 하는데, 해당 라이브러리 이외에 Gson이라는 라이브러리를 이용할 수도 있습니다.

posted by 여성게
:
Web/Spring 2019. 4. 10. 21:29

컨트롤러에서 요청을 엔티티객체로 받는 경우가 있다. 이럴경우 받은 엔티티 객체로 DB까지 로직들이 순차적으로 수행이 될것이다. 그런데 만약 엔티티를 조회하거나, 리스트를 조회하는 경우가 있다고 가정해보자. 그렇다면 요청을 받고 엔티티객체를 조회한 후에 컨트롤러에서 응답값으로 ResponseEntity body에 엔티티객체를 실어 보낼 수 있다. 하지만 여기에서 만약 엔티티객체에서 내가 보내기 싫은 데이터가 포함되어있다면? 그것이 만약 유저정보에 대한 것이고, 그 객체에 패스워드까지 존재한다면? 상상하기 싫은 상황일 것이다. 여기서 해결할 수 있는 방법은 몇가지 있다. 예를 들어 @JsonIgnore,@JsonProperty로 응답을 JSON으로 반환하기 할때 원하는 인스턴스 변수를 제외하고 보낼 수도 있고, 응답용 DTO를 만들어서 응답을 다른 객체로 convert한 후에 보낼 수도 있을 것이다. 어노테이션을 이용한 방법은 추후에 다루어볼것이고, 오늘 다루어 볼 것은 응답용 DTO를 만들어서 응답을 보내는 예제이다. 

 

만약 응답용 DTO를 만들어서 내가 응답으로 내보내고 싶은 정보만 인스턴스변수로 set해서 보내면 될것이다. 하지만 여기서 아주 노가다가 있을 것이다. 그것은 따로 Util 용 Method로 빼서 일일이 set,get하는 방법일 것이다. 만약 한두개의 인스턴스 변수라면 상관없지만 만약 응답으로 내보낼 인스턴수 변수가 아주 많다면 이만한 노가다성 작업도 없을 것이다. 오늘 여기서 소개해줄 해결사가 바로 ModelMapper라는 객체이다. 바로 예제 코드로 들어가겠다.

 

오늘 만들어볼 예제는 이전 포스팅에서 다루어봤던 미완성 GenericController의 응용이다. 자세한 설명은 밑의 링크에서 참고하면 될듯하다.

▶︎▶︎▶︎2019/03/22 - [Spring] - Spring - Springboot GenericController(제네릭컨트롤러), 컨트롤러 추상화

 

1
2
3
4
5
        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>2.3.0</version>
        </dependency>
cs

ModelMapper를 사용하기 위해서 의존성을 추가해준다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class CommonBean {
    
    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}
cs

사용할 ModelMapper클래스를 빈으로 등록해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface DomainMapper {
    
    public <D,E> D convertToDomain(E source,Class<extends D> classLiteral);
}
 
@Component
public class DomainMapperImpl implements DomainMapper{
    
    private ModelMapper modelMapper;
    
    public DomainMapperImpl(ModelMapper modelMapper) {
        this.modelMapper=modelMapper;
    }
    
    /*
     * 공통 매퍼
     */
    @Override
    public <D, E> D convertToDomain(E source, Class<extends D> classLiteral) {
        return modelMapper.map(source, classLiteral);
    }
 
}
cs

그리고 ModelMapper를 이용하여 추후에 많은 유틸메소드(도메인 클래스의 인스턴스를 조작하기 위함)를 만들 가능성이 있을 수도 있기 때문에 따로 유틸로 클래스를 만들어서 해당 클래스내에서 ModelMapper를 활용할 것이다. 여기서는 따로 유틸메소드는 없고 하나의 메소드만 있다. 이것은 엔티티클래스와 매핑할 도메인클래스의 클래스리터럴(Domain.class)를 매개변수로 받아서 ModelMapper.map(엔티티클래스,도메인클래스리터럴) 메소드에 전달한다. 메소드의 반환값으로는 도메인클래스를 반환한다. 여기서 도메인클래스 필드에 엔티티클래스의 필드를 매핑할때는 필드명으로 비교를 한다. 아래에서도 다시 설명하겠지만, 도메인 클래스를 만들때는 엔티티 클래스에서 Http Response로 보내고 싶은 데이터의 필드의 이름과 동일하게 도메인클래스의 필드명으로 만들어줘야하는 것이다.

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
/**
 * BaseEntity 추상클래스
 * 해당 추상클래스를 상속할때 @Tablegenerator는 상속하는 클래스에서 정의해야함
 * 또한 id field의 칼럼속성도 필요할때에 재정의해야함
 * @author yun-yeoseong
 *
 */
@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
@Setter
@Getter
@EqualsAndHashCode(of="id")
public abstract class BaseEntity<extends BaseEntity<?>> implements Comparable<T>{
    
    @Id
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    private long id;
    
    @JsonProperty(access=Access.READ_ONLY)
    @Column(name="CREATED_DATE",nullable=false,updatable=false)
    @CreatedDate
    private LocalDateTime createdDate;
    
    @JsonProperty(access=Access.READ_ONLY)
    @Column(name="UPDATED_DATE",nullable=false)
    @LastModifiedDate
    private LocalDateTime modifiedDate;
    
//    @Column(name="CREATED_BY",updatable=false/*,nullable=false*/)
//    @CreatedBy
    
//    @Column(name="UPDATED_BY",nullable=false)
//    @LastModifiedBy
 
    @Override
    public int compareTo(T o) {
        if(this == o) return 0;
        return Long.compare(this.getId(), o.getId());
    }
 
}
 
/**
 * Main Category Entity
 * @author yun-yeoseong
 *
 */
@Entity
@Table(name="MAIN_CATEGORY"
        ,indexes=@Index(columnList="MAIN_CATEGORY_NAME",unique=false))
@AttributeOverride(name = "id",column = @Column(name = "MAIN_CATEGORY_ID"))
@TableGenerator(name="SEQ_GENERATOR",table="TB_SEQUENCE",
                pkColumnName="SEQ_NAME",pkColumnValue="MAIN_CATEGORY_SEQ",allocationSize=1)
@Getter
@Setter
@ToString
public class MainCategoryEntity extends BaseEntity<MainCategoryEntity> implements Serializable{
    
    private static final long serialVersionUID = 5609501385523526749L;
    
    @NotNull
    @Column(name="MAIN_CATEGORY_NAME",nullable=false)
    private String mainCategoryName;
    
}
cs

이번 예제에서 사용할 엔티티 클래스이다. 우선 공통 엔티티 필드들은 BaseEntity라는 추상클래스로 뺐다. 그리고 정의할 엔티티클래스에서 해당 BaseEntity를 상속하여 정의하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@Setter
@ToString
public class MainCategoryDTO implements Serializable{
 
    private static final long serialVersionUID = 3340033210827354955L;
    
    private Long id;
    
    private String mainCategoryName;
    
}
cs

이번 예제에서 사용할 도메인클래스이다. 엔티티의 생성일과 수정일은 굳이 반환할 필요가 없기때문에 도메인클래스 필드에서 제외하였다. 그러면 엔티티 클래스에서 생성일, 수정일을 제외하고 클라이언트에게 값이 반환될 것이다. 여기서 중요한 것은 앞에서도 한번 이야기 했지만, ModelMapper는 매핑에 사용할 필드들을 이름으로 비교하기 때문에 엔티티클래스에서 사용하는 필드이름과 도메인 클래스에서 매핑할 필드이름이 동일해야한다는 것이다. 이점은 꼭 기억해야 할 것 같다.

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
/**
 * 
 * GenericController - 컨트롤러의 공통 기능 추상화(CRUD)
 * 재정의가 필요한 기능은 @Override해서 사용할것
 * 
 * @author yun-yeoseong
 *
 * @param <E> Entity Type
 * @param <D> Domain Type
 * @param <ID> Entity key type
 */
@Slf4j
public abstract class GenericController<E,D,ID> {
    
    private JpaRepository<E, ID> repository;
    
    private DomainMapper domainMapper;
    
    private Class<D> dtoClass ;
    
    @SuppressWarnings("unchecked")
    public GenericController(JpaRepository<E, ID> repository,DomainMapper domainMapper) {
        
        this.repository = repository;
        this.domainMapper = domainMapper;
        
        /*
         * 타입추론로직
         */
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[1];
        
        if (type instanceof ParameterizedType) {
            this.dtoClass = (Class<D>) ((ParameterizedType) type).getRawType();
        } else {
            this.dtoClass = (Class<D>) type;
        }
 
    }
    
    /*
     * 엔티티아이디로 조회
     */
    @GetMapping("/{id}")
    public  ResponseEntity<D> select(@PathVariable ID id) {
        log.info("GenericController.select - {}",id);
        E e = repository.findById(id).get();
        D d = domainMapper.convertToDomain(e, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.OK);
    }
    
    /*
     * 리스트 조회
     */
    @GetMapping
    public ResponseEntity<List<D>> list(){
        log.info("GenericController.list");
        List<E> lists = repository.findAll();
        List<D> response = lists.stream().map(e->domainMapper.convertToDomain(e, dtoClass)).collect(Collectors.toList());
        return new ResponseEntity<List<D>>(response, HttpStatus.OK);
    }
    
    /*
     * 엔티티 생성
     */
    @Transactional
    @PostMapping
    public ResponseEntity<D> create(@RequestBody E e) {
        log.info("GenericController.create - {}",e.toString());
        log.info("dtoClass type = {}",dtoClass.getName());
        E created = repository.save(e);
        D d = domainMapper.convertToDomain(created, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.CREATED);
    }
    
    /*
     * 엔티티 수정
     */
    @Transactional
    @PutMapping("/{id}")
    public ResponseEntity<D> update(@RequestBody E t) {
        log.info("GenericController.update - {}",t.toString());
        E updated = repository.save(t);
        D d = domainMapper.convertToDomain(updated, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.CREATED);
    }
    
    /*
     * 엔티티 삭제
     */
    @SuppressWarnings("rawtypes")
    @Transactional
    @DeleteMapping("/{id}")
    public ResponseEntity<?> delete(@PathVariable ID id) {
        log.info("GenericController.delete - {}",id);
        repository.deleteById(id);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}
 
cs

오늘 예제로 살펴볼 GenericController이다. 주석에 나와있는 데로 E,D,ID 제네릭타입은 순서대로 엔티티클래스타입,도메인클래스타입,엔티티클래스 ID타입이다. 그리고 위에서 만든 DomainMapper라는 클래스를 이용하여 사용자 요청에 대한 결과값을 조작하여 도메인클래스로 변경하여 리턴하고 있다.(엔티티에서 반환할 데이터만 DTO로 정의해 반환함) 그리고 하나 더 설명할 것은 이전 포스팅에서 다루었던 제네릭 컨트롤러와는 다르게 제네릭 타입으로 도메인클래스 타입을 받을 수 있게 하나 선언하였고, 해당 도메인 클래스의 타입을 추론하는 로직을 생성자에 한번 넣어주었다. 왜냐하면 도메인 클래스는 직접적으로 실체를 매개변수로 받고 있지 않기 때문에 타입을 추론하여 DomainMapper의 메소드의 매개변수로 들어가야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@RestController
@RequestMapping("/category/main")
public class MainCategoryController extends GenericController<MainCategoryEntity, MainCategoryDTO , Long>{
    
    private MainCategoryService mainCategoryService;
 
    public MainCategoryController(JpaRepository<MainCategoryEntity, Long> repository, DomainMapper domainMapper,
            MainCategoryService mainCategoryService) {
        super(repository, domainMapper);
        this.mainCategoryService = mainCategoryService;
    }
    
}
 
cs

마지막으로 제네릭 컨트롤러를 상속한 컨트롤러이다. 단순 CRUD 메소드가 추상화 되어있기 때문에 굉장히 깔끔한 코드가 되었다. 이 예제 코드를 실행시켜보면 반환 객체로 DTO가 나가는 것을 확인할 수 있다. 

 

여기까지 ModelMapper를 다루어봤다. 굉장히 노가다성 작업을 줄여주는 좋은 놈인것 같다. 글을 읽고 혹시 틀린점이 있다면 꼭 지적해주었음 좋겠다.

posted by 여성게
:
Web/Spring 2019. 4. 2. 21:57

Spring boot - Redis를 이용한 HttpSession


오늘의 포스팅은 Spring boot 환경에서 Redis를 이용한 HttpSession 사용법입니다. 무슨 말이냐? 일반 Springframework와는 다르게 Spring boot 환경에서는 그냥 HttpSession을 사용하는 것이 아니고, Redis와 같은 in-memory DB 혹은 RDB(JDBC),MongoDB와 같은 외부 저장소를 이용하여 HttpSession을 이용합니다. 어떻게 보면 단점이라고 볼 수 있지만, 다른 한편으로는 장점?도 존재합니다. 일반 war 형태의 배포인 Dynamic Web은 같은 애플리케이션을 여러개 띄울 경우 세션 공유를 위하여 WAS단에서 Session Clustering 설정이 들어갑니다. 물론 WAS 설정에 익숙한 분들이라면 별 문제 없이 설정가능하지만, WAS설정 등에 미숙하다면 확실함 없이 구글링을 통하여 막 찾아서 설정을 할 것입니다. 물론 나쁘다는 것은 아닙니다. 벤치마킹 또한 하나의 전략이니까요. 하지만 Spring boot의 경우 Session Cluster를 위하여 별도의 설정은 필요하지 않습니다. 이중화를 위한 같은 애플리케이션 여러개가 HttpSession을 위한 같은 저장소만 바라보면 됩니다. 어떻게 보면 설정이 하나 추가된 것이긴 하지만 익숙한 application.properties등에 설정을 하니, 자동완성도 되고... 실수할 일도 줄고, 디버깅을 통해 테스트도 가능합니다. 크게 중요한 이야기는 아니므로 바로 예제를 들어가겠습니다.



테스트 환경

  • Spring boot 2.1.3.RELEASE(App1,App2)
  • Redis 5.0.3 Cluster(Master-6379,6380,6381 Slave-6382,6383,6384)

만약 Redis Cluster환경을 구성한 이유는, 프로덕 환경에서는 Redis 한대로는 위험부담이 있기때문에 고가용성을 위하여 클러스터 환경으로 테스트를 진행하였습니다. 한대가 죽어도 서비스되게 하기 위해서이죠. 만약 한대로 하시고 싶다면 한대로 진행하셔도 됩니다. 하지만 클러스터환경을 구성하고 싶지만 환경구성에 대해 잘 모르시는 분은 아래 링크를 참조하여 구성하시길 바랍니다.


▶︎▶︎▶︎2019/03/01 - [Redis] - Springboot,Redis - Springboot Redis Nodes Cluster !(레디스 클러스터)

▶︎▶︎▶︎2019/02/28 - [Redis] - Redis - Cluster & Sentinel 차이점 및 Redis에 대해


애플리케이션은 총 2대를 준비하였고, 한대를 클라이언트 진입점인 API G/W, 한대를 서비스 애플리케이션이라고 가정하고 테스트를 진행하였습니다. 즉, 두 애플리케이션이 하나의 세션을 공유할 수 있을까라는 궁금즘을 해결하기 위한 구성이라고 보시면 됩니다. 사실 같은 애플리케이션을 이중화 구성을 한 것이 아니고 별도의 2개의 애플리케이션끼리 세션을 공유해도 되는지는 아직 의문입니다. 하지만 다른 애플리케이션끼리도 HttpSession을 공유할 수 있다면 많은 이점이 있을 것같아서 진행한 테스트입니다.


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
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
cs


두개의 애플리케이션에 동일하게 의존 라이브러리를 추가해줍니다. 저는 부트 프로젝트 생성시 Web을 체크하였고, 나머지 위에 4개는 수동으로 추가해주었습니다.


1
2
3
spring.session.store-type=redis
spring.redis.cluster.nodes=127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381
 
cs


application.properties입니다. 이 또한 두개의 애플리케이션에 동일하게 넣어줍니다. 간단히 설정에 대해 설명하면 spring.session.store-type=redis는 HttpSession 데이터를 위한 저장소를 Redis를 이용하겠다는 설정입니다. Redis 말고도 MongoDB,JDBC등이 있습니다. 두번째 spring.redis.cluster.nodes=~설정은 저장소로 사용할 Redis의 클러스터 노드 리스트(마스터)를 넣어줍니다. 


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
/**
 * Redis Cluster Config
 * @author yun-yeoseong
 *
 */
@Component
@ConfigurationProperties(prefix = "spring.redis.cluster")
public class RedisClusterConfigurationProperties {
    
    /**
     * spring.redis.cluster.nodes[0]=127.0.0.1:6379
     * spring.redis.cluster.nodes[1]=127.0.0.1:6380
     * spring.redis.cluster.nodes[2]=127.0.0.1:6381
     */
    private List<String> nodes;
 
    public List<String> getNodes() {
        return nodes;
    }
 
    public void setNodes(List<String> nodes) {
        this.nodes = nodes;
    }
    
    
}
cs


Redis 설정을 위하여 클러스터 노드리스트 값을 application.proerties에서 읽어올 빈입니다.


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
@Configuration
public class RedisConfig {
    /**
     * Redis Cluster 구성 설정
     */
    @Autowired
    private RedisClusterConfigurationProperties clusterProperties;
 
    /**
     * JedisPool관련 설정
     * @return
     */
    @Bean
    public JedisPoolConfig jedisPoolConfig() {
        return new JedisPoolConfig();
    }
    
    /**
     * Redis Cluster 구성 설정 - Cluster 구성
     */
    @Bean
    public RedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig) {
        return new JedisConnectionFactory(new RedisClusterConfiguration(clusterProperties.getNodes()),jedisPoolConfig);
    }
        
}
cs


JedisPoolConfig 및 RedisConnectionFacotry 빈입니다. 아주 작동만 할 수 있는 기본입니다. 추후에는 적절히 설정값을 넣어서 성능 튜닝이 필요합니다.


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
@Slf4j
@EnableRedisHttpSession
@RestController
@SpringBootApplication
public class SessionWebTest1Application {
 
    public static void main(String[] args) {
        SpringApplication.run(SessionWebTest1Application.class, args);
    }
    
    @GetMapping("/request")
    public String getCookie(HttpSession session) {
        String sessionKey = session.getId();
        session.setAttribute("ID""yeoseong_yoon");
        log.info("set userId = {}","yeoseong_yoon");
        
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders header = new HttpHeaders();
        header.add("Cookie""SESSION="+redisSessionId);
        HttpEntity<String> requestEntity = new HttpEntity<>(null, header);
        
        ResponseEntity<String> cookieValue = restTemplate.exchange("http://localhost:8090/request",HttpMethod.GET ,requestEntity ,String.class);
        return "server1_sessionKey : "+session.getId()+"<br>server2_sessionKey : "+cookieValue.getBody();
    }
    
}
 
cs


App1의 클래스입니다. 우선 로그를 찍기위해 lombok 어노테이션을 사용하였고, Redis를 이용한 HttpSession 사용을 위해 @EnableRedisHttpSession 어노테이션을 선언하였습니다. 여기서 조금 특이한 점은 RestTemplate 요청에 SESSION이라는 쿠키값을 하나 포함시켜 보내는 것입니다. 잘 생각해보면 일반 웹프로젝트에서는 세션객체의 식별을 위해 JSESSIONID라는 쿠키값을 이용합니다. 이것과 동일한 용도로 Redis HttpSession은 SESSION이라는 쿠키값을 이용하여 자신의 HttpSession 객체를 식별합니다. 즉, App2에서도 동일한 HttpSession객체 사용을 위하여 SESSION 쿠키값을 보내는 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@EnableRedisHttpSession
@RestController
@SpringBootApplication
public class SessionWebTest2Application {
 
    public static void main(String[] args) {
        SpringApplication.run(SessionWebTest2Application.class, args);
    }
    
    @GetMapping("/request")
    public String getCookie(HttpSession session) {
        log.info("get userId = {}",session.getAttribute("ID"));
        System.out.println(session.getAttribute("ID"));
        System.out.println(session.getId());
        return session.getId();
    }
    
}
cs

App2번의 클래스입니다. App1에서 보낸 요청을 받기위한 컨트롤러가 존재합니다. 결과값으로는 HttpSession의 Id값을 리턴합니다. 그리고 App1의 컨트롤러에서는 App2번이 보낸 세션 아이디와 자신의 세션아이디를 리턴합니다. 


브라우저에서 요청한 최종 결과입니다. 두 애플리케이션의 HttpSession ID 값이 동일합니다.


각각 애플리케이션의 로그입니다. App1번에서 yeoseong_yoon이라는 데이터를 세션에 추가하였고, 해당 데이터를 App2번에서 잘 가져오는 것을 볼 수 있습니다.



마지막으로 Redis 클라이언트 명령어를 이용해 진짜 Redis에 세션관련 데이터가 들어가있는지 확인해보니 잘 들어가있습니다. (Redis serialization 설정을 적절히 맞추지 않아 yeoseong_yoon이라는 데이터 앞에 알 수 없게 인코딩된 데이터가 있내요..) 


여기까지 Spring boot환경에서 Redis를 이용한 HttpSession 사용방법이었습니다. 혹시 틀린점이 있다면 코멘트 부탁드립니다.

posted by 여성게
:
Web/Spring 2019. 3. 22. 15:45

Spring - Springboot GenericController(제네릭컨트롤러), 컨트롤러 추상화



Web applcation을 개발하면 공통적으로 개발하는 Flow가 있다.



Controller->Service->Repository는 거의 왠만한 웹어플리케이션의 개발 플로우일 것이다. 하지만 이런 플로우 안에서도 거의 모든 비즈니스마다 공통적인 로직이있다. 바로 CRUD이다. 모든 도메인에는 생성,수정,삭제,조회 로직이 들어간다. 이러한 로직이 모든 컨트롤러,서비스 클래스에 도메인마다 작성이 된다면 이것도 중복코드인 것이다. 오늘 포스팅할 주제는 바로 이러한 로직을 추상화한 Generic Controller이다. 사실 아직 많이 부족한 탓에 더 좋은 방법도 있겠지만 나름 혼자 고심하여 개발한 것이기 때문에 만약 잘못된 부분이 있다면 지적을 해주셨음 좋겠다.





Entity Class


1
2
3
4
5
6
7
8
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
 
@EnableJpaAuditing
@Configuration
public class JpaConfig {
 
}
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
import java.time.LocalDateTime;
 
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
 
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
 
import lombok.Getter;
import lombok.Setter;
 
/**
 * BaseEntity 추상클래스
 * 해당 추상클래스를 상속할때 @Tablegenerator는 상속하는 클래스에서 정의해야함
 * 또한 id field의 칼럼속성도 필요할때에 재정의해야함
 * @author yun-yeoseong
 *
 */
@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
@Setter
@Getter
public abstract class BaseEntity<extends BaseEntity<?>> implements Comparable<T>{
    
    @Id
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "RNB_SEQ_GENERATOR")
    private long id;
    
    @Column(name="CREATED_DATE",nullable=false,updatable=false)
    @CreatedDate
    private LocalDateTime createdDate;
    
    @Column(name="UPDATED_DATE",nullable=false)
    @LastModifiedDate
    private LocalDateTime modifiedDate;
 
    @Override
    public int compareTo(T o) {
        Long result = getId()-o.getId();
        return result.intValue();
    }
    
}
 
cs


엔티티마다도 공통적인 필드를 가지고 있으므로 BaseEntity 클래스 하나를 정의하였다. 정렬을 위한 compareTo는 자신에 맞게 재정의하면 될 것 같다. @EntityListener 어노테이션을 추가해 AuditingEntityListener 사용을 명시한다. 해당 어노테이션을 붙인 후에 @CreatedDate는 엔티티 생성시점 처음에 생성되는 날짜필드가 되는 것이고, @LastModifiedDate는 엔티티 수정이 될때 매번 수정되는 날짜 필드이다.(@PrePersist,@PreUpdate 등을 사용해도 될듯) 사실 이 두개의 어노테이션말고 @CreatedBy@LastModifiedBy 어노테이션도 존재한다. 이것을 생성자와 수정자를 넣어주는 어노테이션인데, 이것은 스프링 시큐리티의 Principal을 사용한다. 이것은 당장 필요하지 않으므로 구현하지 않았다.(구글링 참조) 그리고 마지막으로는 @Id 필드이다. 해당 생성 전략을 테이블 타입을 이용하였다. 만약 더욱 다양한 타입의 @Id가 필요하다면 아래 링크를 참조하자.


▶︎▶︎▶︎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
import java.io.Serializable;
 
import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Index;
import javax.persistence.Table;
import javax.persistence.TableGenerator;
 
import com.web.rnbsoft.common.jpa.BaseEntity;
 
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
 
/**
 * Intent Entity
 * @author yun-yeoseong
 *
 */
@Entity
@Table(name="RNB_INTENT"
        ,indexes=@Index(columnList="INTENT_NAME",unique=false))
@AttributeOverride(name = "id",column = @Column(name = "INTENT_ID"))
@TableGenerator(name="RNB_SEQ_GENERATOR",table="TB_SEQUENCE",
                pkColumnName="SEQ_NAME",pkColumnValue="RNB_INTENT_SEQ",allocationSize=1)
@Getter
@Setter
@ToString
public class IntentEntity extends BaseEntity<IntentEntity> implements Serializable{
    
    private static final long serialVersionUID = 1864304860822295551L;
    
    @Column(name="INTENT_NAME",nullable=false)
    private String intentName;
    
}
 
cs


BaseEntity를 상속한 엔티티클래스이다. 몇가지 설명을 하자면 우선 BaseEntity에 있는 id 필드를 오버라이드 했다는 것이고, @TableGenerator를 선언하여 BaseEntity의 @GenerateValue에서 참조하여 기본키를 생성할 수 있게 하였다.



Service


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
import com.web.rnbsoft.entity.intent.IntentEntity;
 
/**
 * 
 * @author yun-yeoseong
 *
 */
public interface IntentService {
    public IntentEntity findByName(String name);
}
 
/************************************************************/
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import com.web.rnbsoft.entity.intent.IntentEntity;
import com.web.rnbsoft.repository.intent.IntentRepository;
 
import lombok.extern.slf4j.Slf4j;
 
/**
 * 
 * @author yun-yeoseong
 *
 */
@Slf4j
@Service
public class IntentServiceImpl implements IntentService {
    
    @Autowired
    private IntentRepository intentRepository;
    
    @Override
    public IntentEntity findByName(String name) {
        log.debug("IntentServiceImple.findByName - {}",name);
        return intentRepository.findByIntentName(name);
    }
    
    
}
 
cs


Repository


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
import org.springframework.data.jpa.repository.JpaRepository;
 
import com.web.rnbsoft.entity.intent.IntentEntity;
 
/**
 * IntentRepository
 * @author yun-yeoseong
 *
 */
public interface IntentRepository extends JpaRepository<IntentEntity, Long>, IntentRepositoryCustom{
    public IntentEntity findByIntentName(String intentName);
}
 
 
/************************************************************/
 
import java.util.List;
 
import com.web.rnbsoft.entity.intent.IntentEntity;
 
/**
 * 
 * @author yun-yeoseong
 *
 */
public interface IntentRepositoryCustom {
    public List<IntentEntity> selectByCategoryAndName(String category,String name);
}
 
/************************************************************/
 
import java.util.List;
 
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
 
import com.querydsl.jpa.JPQLQuery;
import com.web.rnbsoft.entity.intent.IntentEntity;
import com.web.rnbsoft.entity.intent.QIntentEntity;
 
/**
 * 
 * @author yun-yeoseong
 *
 */
public class IntentRepositoryImpl extends QuerydslRepositorySupport implements IntentRepositoryCustom {
 
    public IntentRepositoryImpl() {
        super(IntentEntity.class);
    }
 
    @Override
    public List<IntentEntity> selectByCategoryAndName(String category, String name) {
        
        QIntentEntity intent = QIntentEntity.intentEntity;
        
        JPQLQuery<IntentEntity> query = from(intent)
                          .where(intent.intentName.contains(name));
                          
        return query.fetch();
    }
 
}
cs


Repository는 QueryDSL을 이용하여 작성하였다. QueryDSL 설정법 등은 아래 링크를 참조하자

▶︎▶︎▶︎Spring JPA+QueryDSL

▶︎▶︎▶︎QueryDSL Query작성법




Controller


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
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
 
import lombok.extern.slf4j.Slf4j;
 
/**
 * 
 * GenericController - 컨트롤러의 공통 기능 추상화(CRUD)
 * 
 * @author yun-yeoseong
 *
 */
@Slf4j
public abstract class GenericController<T ,ID> {
    
    @Autowired
    private JpaRepository<T, ID> repository;
    
    @GetMapping("/{id}")
    public T select(@PathVariable ID id) {
        log.debug("GenericController.select - {}",id);
        return repository.findById(id).get();
    }
    
    @GetMapping
    public List<T> list(){
        log.debug("GenericController.list");
        return repository.findAll();
    }
    
    @PostMapping
    public T create(@RequestBody T t) {
        log.debug("GenericController.create - {}",t.toString());
        T created = repository.save(t);
        return created;
    }
    
    @PutMapping("/{id}")
    public T update(@RequestBody T t) {
        log.debug("GenericController.update - {}",t.toString());
        T updated = repository.save(t);
        return updated;
    }
    
    @DeleteMapping("/{id}")
    public boolean delete(@PathVariable ID id) {
        log.debug("GenericController.delete - {}",id);
        repository.deleteById(id);
        return true;
    }
}
cs


세부로직은 신경쓰지말자. 이것이 공통적으로 컨트롤러가 사용하는 것을 추상화한 GenericController이다. 사실 Service 단까지도 GenericService를 구현하려고 했으나, 크게 복잡한 로직도 아니고 해서 전부 Repository를 주입받아서 사용하였다. 나중에 이 틀을 가지고 다듬어서 사용하면 될듯 싶다.


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
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import com.web.rnbsoft.controller.GenericController;
import com.web.rnbsoft.entity.intent.IntentEntity;
import com.web.rnbsoft.service.intent.IntentService;
 
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
@RestController
@RequestMapping("/intent")
public class IntentController extends GenericController<IntentEntity,Long> {
    
    private IntentService service;
    
    public IntentController(IntentService service) {
        this.service = service;
    }
    
    @PostMapping("/search")
    public IntentEntity selectByName(@RequestBody String intentName) {
        log.debug("IntentController.selectByName - {}",intentName);
        return service.findByName(intentName);
    }
}
 
cs


GenericController를 상속받는 Controller이다. CRUD의 공통로직이 모두 제거되었고 물론 이 컨트롤러 말고도 다른 컨트롤러 또한 모두 CRUD 로직은 제거 될것이다. 훨씬 코드가 간결해졌다. 여기서 하나더 기능을 추가하자면 모든 예외처리를 한곳에서 처리해버리는 것이다. 


RestControllerAdvice


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
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
 
import com.fasterxml.jackson.core.JsonProcessingException;
 
import lombok.extern.slf4j.Slf4j;
 
/**
 * RestControllerAdvice
 * RestController 공통 예외처리
 * @author yun-yeoseong
 *
 */
@Slf4j
@RestControllerAdvice("com.web.rnbsoft")
public class RestCtrlAdvice {
    @ExceptionHandler(value = {Exception.class})
    protected ResponseEntity<String> example(RuntimeException exception,
            Object body,
            WebRequest request) throws JsonProcessingException {
        log.debug("RestCtrlAdvice");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{\"message\":\"example\"}");
    }
}
 
cs

RestController의 모든 공통 예외처리를 해당 클래스에 정의한다. 물론 추후에는 메시지소스를 이용해 응답까지 하나로 통일할 수도 있을 것같다.

여기까지 GenericController를 구현해봤다. 많이 부족한 점이 많은 코드이기에 많은 지적을 해주셨으면 하는 바람이다.

posted by 여성게
:
Web/Spring 2019. 2. 25. 15:57

Spring - ApplicationContext,ApplicationContextAware, 빈이 아닌 객체에 빈주입할때!



@Autuwired,@Inject 등의 어노테이션으로 의존주입을 하기 위해서는 해당 객체가 빈으로 등록되어 있어야만 가능하다.

사실 이런 상황은 웹프로그래밍에서는 거의 없겠지만... 빈으로 등록되지 않은 객체에 빈으로 등록된 객체를 의존주입해야할 상황이 있을 수도 있다.

 그럴때 사용할수 있는 하나의 UtilClass 이다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class BeanUtils implements ApplicationContextAware {
 
    private static ApplicationContext context;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // TODO Auto-generated method stub
        context = applicationContext;
    }
 
    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }
}
cs


ApplicationContextAware를 구현한 BeanUtils 클래스를 하나 만들었다. 그리고 setApplicationContext() 메소드로 ApplicationContext를 

주입받고 있는 상황이다. 그리고 static으로 선언된 getBean 메소드를 이용하여 빈주입을 원하는 어딘가에서


BeanUtils.getBean()를 호출하여 빈을 주입받을 수 있다.!

posted by 여성게
:
Web/Spring 2019. 2. 21. 22:14

Springboot - CommandLineRunner(커맨드라인러너)



Springboot에서 서버 구동 시점에 초기화작업으로 무엇인가를 넣고 싶다면 사용할 수 있는 방법 중 하나가 

CommandLineRunner인터페이스를 상속받는 것이다.


@SpringBootApplication 어노테이션이 붙어있는 부트구동 클래스에 CommandLineRunner를 implements

하고 run(String... strings)를 Override한다.

그리고 해당 run()에 애플리케이션의 초기작업(빈등록 과정 등등)이후 실행할 로직을 작성해주면 된다.




예제 코드는 항공편 예약서비스 예제인데, 애플리케이션 구동 시점에 예약가능한

비행편을 알아보기위하여 검색 마이크로서비스 애플리케이션에 초기데이터를 

삽입하는 코드이다.



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
@SpringBootApplication
public class Application implements CommandLineRunner {
    private static final Logger logger = LoggerFactory.getLogger(Application.class);
    
    @Autowired
    private FlightRepository flightRepository;
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    @Override
    public void run(String... strings) throws Exception {
        List<Flight> flights = new ArrayList<>();
        flights.add(new Flight("BF100""SEA","SFO","22-JAN-18",new Fares("100""USD"),new Inventory(100)));
        flights.add(new Flight("BF101""NYC","SFO","22-JAN-18",new Fares("101""USD"),new Inventory(100)));
        flights.add(new Flight("BF105""NYC","SFO","22-JAN-18",new Fares("105""USD"),new Inventory(100)));
        flights.add(new Flight("BF106""NYC","SFO","22-JAN-18",new Fares("106""USD"),new Inventory(100)));
        flights.add(new Flight("BF102""CHI","SFO","22-JAN-18",new Fares("102""USD"),new Inventory(100)));
        flights.add(new Flight("BF103""HOU","SFO","22-JAN-18",new Fares("103""USD"),new Inventory(100)));
        flights.add(new Flight("BF104""LAX","SFO","22-JAN-18",new Fares("104""USD"),new Inventory(100)));
        
        flightRepository.saveAll(flights);
        
        logger.info("Looking to load flights...");
        for (Flight flight : flightRepository.findByOriginAndDestinationAndFlightDate("NYC""SFO""22-JAN-18")) {
            logger.info(flight.toString());
        }
    }
     
}
cs


만약 애플리케이션 구동 순서에 선후가 존재한다면 후행 애플리케이션에 위와같이 run()을 오버라이드하여

선행 애플리케이션에 Health Check등을 하는 로직을 넣어도 괜찮은 방법이 될 것 같다.

posted by 여성게
: