저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링4 입문

스프링 4

DDD Start

객체 지향과
디자인 패턴

JSP 2.3

JPA 입문
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

HTTP 요청 헤더를 이용해서 접속한 사용자 정보를 받기로 했다. 받아야 할 정보가 두 개여서 스프링 컨트롤러에서 다음과 같은 코드를 사용하게 되었다.


@RestController

public class HandoverApi {


    private HandoverService handoverService;


    @PostMapping("/api/handover")

    public HandoverPk postHandover(

            @RequestHeader(name = "employeeId", required = false) String employeeIdHeader,

            @RequestParam(name = "employeeId", required = false) String employeeIdParam,

            @RequestHeader(name = "cellphone", required = false) String cellphoneHeader,

            @RequestParam(name = "cellphone", required = false) String cellphoneParam,

            @Valid HandoverRequest req) throws ServletRequestBindingException {

        if (StringUtils.isEmpty(employeeIdHeader) && StringUtils.isEmpty(employeeIdParam)) {

            throw new ServletRequestBindingException("Missing employeeId");

        }

        if (StringUtils.isEmpty(cellphoneHeader) && StringUtils.isEmpty(cellphoneParam)) {

            throw new ServletRequestBindingException("Missing cellphoneParam");

        }

        String employeeId = employeeIdParam != null ? employeeIdParam : employeeIdHeader;

        String cellphone = cellphoneParam != null ? cellphoneParam : cellphoneHeader;

        req.setEmployeeId(employeeId);

        req.setCellphone(cellphone);


        return handoverService.handoverDigWork(req);

    }


employeeId와 cellphone 값을 요청 헤더나 요청 파라미터로 받을 수 있도록 했다. 헤더와 파라미터를 함께 사용할 수 있게 해서 코드가 다소 장황해졌다. 문제는 이런 코드가 계속해서 중복해서 출현하게 되었다는 것이다. employeeId와 cellphone 값이 필요한 API에서 이런 장황하면서 보기 싫은 코드를 중복해서 사용하게 되었다.


이런 중복을 없애기 위해 스프링의 커스텀 HandlerMethodArgumentResolver를 사용했다. 원하는 코드 모양은 다음과 같다.


@RestController

public class HandoverApi {


    @PostMapping("/api/handover")

    public HandoverPk postHandover(

            @MobileUser(check = MobileUserCheck.ALL) MobileUserInfo mobUserInfo,

            @Valid DigWorkHandoverRequest req) {

        req.setEmployeeId(mobUserInfo.getEmployeeId());

        req.setCellphone(mobUserInfo.getCellphone());


        return handoverService.handoverDigWork(req);

    }


@MobileUser 애노테이션이 붙은 MobileUserInfo 타입 파라미터에 자동으로 employeeId와 cellphone 값이 담기도록 스프링을 확장하는 것을 목표로 했다.


파라미터에 사용할 MobileUserInfo와 @MobileUser


먼저 employeeId와 cellphone을 담을 데이터 클래스를 하나 만들었다.


@Getter

@AllArgsConstructor

@ToString

public class MobileUserInfo {

    private String employeeId;

    private String cellphone;

    

    public void checkEmployeeId() throws ServletRequestBindingException {

        if (StringUtils.isEmpty(employeeId))

            throw new ServletRequestBindingException("Missing employeeId");

    }

    

    public void checkCellphone() throws ServletRequestBindingException {

        if (StringUtils.isEmpty(cellphone))

            throw new ServletRequestBindingException("Missing cellphone");

    }


    public void checkAll() throws ServletRequestBindingException {

        checkEmployeeId();

        checkCellphone();

    }

    

}


@MobileUser 애노테이션은 다음과 같이 정의했다.


@Target(value = { ElementType.PARAMETER })

@Retention(value = RetentionPolicy.RUNTIME)

public @interface MobileUser {


    MobileUserCheck check() default MobileUserCheck.EMPLOYEE_ID;


}


check 속성은 값을 어디까지 검사할지 여부를 지정하기 위한 용도로 사용한다. 이 예제의 경우 employeeId만 필요한 경우가 있고, cellphone만 필요한 경우가 있고, 둘 다 필요한 경우도 있다. 이를 필요에 따라 검사 대상을 지정할 수 있도록 check 속성을 추가했다. MobileUserCheck 열거 타입은 검사할 대상을 포함한다.


public enum MobileUserCheck {

    ALL, EMPLOYEE_ID, CELLPHONE, NONE

}


커스텀 HandlerMethodArgumentResolver 구현


스프링이 컨트롤러 메서드의 인자로 MobileUserInfo 타입 객체를 받을 수 있으려면 HandlerMethodArgumentResolver의 구현체를 알맞게 제공해야 한다. 예제를 위한 구현체는 다음과 같다.


public class MobileUserInfoResolver implements HandlerMethodArgumentResolver {

    @Override

    public boolean supportsParameter(MethodParameter parameter) {

        MobileUser mobUserAnnot = parameter.getParameterAnnotation(MobileUser.class);

        return mobUserAnnot != null && 

                 MobileUserInfo.class.isAssignableFrom(parameter.getParameterType());

    }


    @Override

    public Object resolveArgument(MethodParameter parameter, 

            ModelAndViewContainer mavContainer,

            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) 

            throws Exception {

        String employeeIdHeader = webRequest.getHeader("employeeId");

        String employeeIdParam = webRequest.getParameter("employeeId");

        String cellphoneHeader = webRequest.getHeader("cellphone");

        String cellphoneParam = webRequest.getParameter("cellphone");


        String employeeId = employeeIdParam != null ? employeeIdParam : employeeIdHeader;

        String cellphone = cellphoneParam != null ? cellphoneParam : cellphoneHeader;


        MobileUser mobUserAnnot = parameter.getParameterAnnotation(MobileUser.class);

        MobileUserInfo mobileUserInfo = new MobileUserInfo(employeeId, cellphone);

        switch (mobUserAnnot.check()) {

        case ALL:

            mobileUserInfo.checkAll();

            break;

        case EMPLOYEE_ID:

            mobileUserInfo.checkEmployeeId();

            break;

        case CELLPHONE:

            mobileUserInfo.checkCellphone();

            break;

        default:

            break;

        }

        return mobileUserInfo;

    }

}


supportsParameter() 메서드는 컨트롤러 메서드의 특정 파라미터를 지원하는지 여부를 리턴한다. 이 예제의 경우 파라미터에 MobileUser 애노테이션이 붙이 있고 파라미터 타입이 MobileUserInfo인 경우 true를 리턴하도록 구현했다.


resolveArgument() 메서드는 파라미터에 전달할 객체를 생성한다. 이 예에서는 employeeId와 cellphone 값을 요청 헤더나 요청 파라미터에서 읽어와 MobileUserInfo를 생성하고, @MobileUser 애노테이션의 check 값에 따라 값 검사를 수행한 뒤에, 검사에 통과하면 MobileUserInfo를 리턴한다.


WebMvcConfigurer로 설정하기


마지막으로 준비할 작업은 커스텀 HandlerMethodArgumentResolver를 사용하도록 스프링 MVC를 설정하는 것이다. @EnableWebMvc나 스프링 부트를 사용한다면 다음과 같이 WebMvcConfigurer 구현 클래스를 사용해서 커스텀 HandlerMethodArgumentResolver를 등록하면 된다.


@Configuration

public class WebMvcCustomConfiguration extends WebMvcConfigurerAdapter {


    @Override

    public void addArgumentResolvers(

            List<HandlerMethodArgumentResolver> argumentResolvers) {

        argumentResolvers.add(new MobileUserInfoResolver());

    }

    

}



커스텀 인자 사용


이제 컨트롤러 메서드의 파라미터로 커스텀 인자 타입을 사용하면 된다.


@PostMapping("/api/handover")

public HandoverPk postHandover(

        @MobileUser(check = MobileUserCheck.ALL) MobileUserInfo mobUserInfo,

        @Valid DigWorkHandoverRequest req) {

    req.setEmployeeId(mobUserInfo.getEmployeeId());

    req.setCellphone(mobUserInfo.getCellphone());


    return handoverService.handoverDigWork(req);

}


@GetMapping(value = "/api/checks")

public yCheckData getCheckData(@MobileUser MobileUserInfo userInfo) {

    CheckData checks = checkService.getChecks(userInfo.getEmployeeId());

    return checks;

}


Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

Spring Data Jpa의 Speicfication을 애용하는 편인데, 이 Specification을 사용해서 조건을 조합하다보면 다음과 같은 코드를 종종 작성하게 된다. (관련 내용은 http://javacan.tistory.com/entry/SpringDataJPA-Specifcation-Usage 참고)


Specifications<Check> specs = Specifications.where(

    CheckSpecs.yearQuarter(searchRequest.getYear(), searchRequest.getQuarter()));


if (searchRequest.hasTeamCd())

    specs = specs.and(CheckSpecs.teamCd(searchRequest.getTeamCd()));

if (searchRequest.hasPlanDate())

    specs = specs.and(CheckSpecs.planDate(searchRequest.getPlanDate()));


List<Check> checks = checkRepository.findAll(specs);


if 절과 각 Spec을 and로 엮는 코드가 실수하기 좋게 되어 있다. 이를 보완하고자 SpecBuilder라는 보조 클래스를 하나 만들었다. 이 클래스를 사용하면 위 코드를 다음과 같이 변경할 수 있다.


Specification<Check> spec = SpecBuilder.builder(Check.class)

        .and(CheckSpecs.yearQuarter(searchRequest.getYear(), searchRequest.getQuarter()))

        .whenHasText(searchRequest.getTeamCd(), str -> CheckSpec.teamCd(str))

        .whenHasText(searchRequest.getPlanDate(), CheckSpec::planDate)

        .toSpec();


.List<Check> checks = checkRepository.findAll(specs);


단순히 and로 조합하는 경우, if를 사용할 때보다 코드를 보기가 더 좋아졌다.


SpecBuilder의 완전한 코드는 다음과 같다.


public class SpecBuilder {


    public static <T> SpecSupplier<T> builder(Class<T> type) {

        return new SpecSupplier<T>();

    }


    public static class Builder<T> {

        private List<Specification<T>> specs = new ArrayList<>();


        public Builder<T> and(Specification<T> spec) {

            specs.add(spec);

            return this;

        }


        public Builder<T> whenHasText(String str, 

                 Function<String, Specification<T>> specSupplier) {

            if (StringUtils.hasText(str)) {

                specs.add(specSupplier.apply(str));

            }

            return this;

        }


        public Builder<T> when(String str, 

                 Function<String, Specification<T>> specSupplier) {

            specs.add(specSupplier.apply(str));

            return this;

        }

        

        public Builder<T> whenHasTextThenBetween(String from, String to,

                BiFunction<String, String, Specification<T>> specSupplier) {

            if (StringUtils.hasText(from) && StringUtils.hasText(to)) {

                specs.add(specSupplier.apply(from, to));

            }

            return this;

        }


        public Builder<T> whenIsTrue(Boolean cond,

                Supplier<Specification<T>> specSupplier) {

            if (cond != null && cond.booleanValue()) {

                specs.add(specSupplier.get());

            }

            return this;

        }


        public Specification<T> toSpec() {

            if (specs.isEmpty())

                return Specifications.where(null);

            else if (specs.size() == 1)

                return specs.get(0);

            else {

                return specs.stream().reduce(

                        Specifications.where(null),

                        (specs, spec) -> specs.and(spec),

                        (specs1, specs2) -> specs1.and(specs2));

            }

        }

    }

}


Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

최근 진행하는 프로젝트는 DB 관련 기술로 JPA를 사용하고 있는데, 연동 대상은 레거시 DB이다. 레기서 DB답게 복합키를 갖는 테이블이 다수 존재한다. 아래는 한 예이다.



이 테이블은 특정 업무의 점검 결과를 저장할 때 사용하는데(간결함을 위해 이름을 변경하고 칼럼수도 줄였다), CHECK_H는 점검 결과에 대한 마스터 테이블이고, CHECK_D는 각 세부 점검 항목의 결과를 저장하는 테이블이다. CHECK_H와 CHECK_D는 1:N의 관계를 갖는다.


그림에서 보는 것처럼 CHECK_H의 PK는 네 개의 칼럼을 구성되어 있으며, CHECK_D는 이 네 칼럼을 참조한다.


검사 항목은 20여개 정도 되는데, 각 항목을 그룹으로 나눠서 표현한다. 각 항목이 속한 그룹을 저장하는 칼럼이 GROUP이고 검사 항목을 저장하는 칼럼이 ITEM이며, 그 결과를 저장한 칼럼이 RSLT이다. 화면에 결과를 표시할 때에는 GROUP과 ITEM을 오름차순으로 정렬해서 출력한다.


CHECK_H와 CHECK_D는 개념적으로 하나의 검사 결과를 의미하므로, 이 둘과 매핑되는 모델은 하나의 애그리거트에 포함된다. 매핑할 애그리거트를 다음과 같이 만들었다.



CHECK_D는 별도 라이프사이클이 없고 CHECK_H에 종속되어 있으므로 CHECK_D에 매핑되는 Detail을 밸류로 표현했고, Check를 애그리거트의 루트로 했다. 복합키를 사용하므로 복합키에 해당하는 식별자 클래스인 CheckId도 따로 만들었다.


Detail 클래스와 CHECK_D 테이블의 매핑 설정


Detail은 밸류이므로 CHECK_D의 주요키와 매핑할 필요가 없으므로 밸류가 가져야 할 속성만 정의했다.


@Embeddable

public class Detail {

    private String group;

    private String item;

    private String rslt;


    ...

}


Check 클래스와 CHECK_H 테이블의 매핑 설정


복합키를 위한 CheckId는 다음과 같다.


@Embeddable

public class CheckId implements Serializable {

    

    @Column(name = "JOIN_NUM")

    private String joinNum;


    @Column(name = "PATH_FLAG")

    private String pathFlag;


    @Column(name = "JOIN_YMD")

    private String joinYmd;


    @Column(name = "RSLT_FLAG")

    private String rsltFlag;

}


다음은 Check 클래스 설정이다.


@Entity

@Table(name = "CHECK_H")

public class Check {

    @EmbeddedId

    private CheckId id;


    @Column(name = "FROM_TIME")

    private String fromTime;


    @Column(name = "TO_TIME")

    private String toTime;


    @ElementCollection(fetch = FetchType.EAGER)

    @CollectionTable(name = "CHECK_D", joinColumns = {

            @JoinColumn(name = "JOIN_NUM", referencedColumnName = "JOIN_NUM"),

            @JoinColumn(name = "PATH_FLAG", referencedColumnName = "PATH_FLAG"),

            @JoinColumn(name = "JOIN_YMD", referencedColumnName = "JOIN_YMD"),

            @JoinColumn(name = "RSLT_FLAG", referencedColumnName = "RSLT_FLAG") })

    @org.hibernate.annotations.OrderBy(clause = "GROUP asc, ITEM asc")

    private Set<CheckDetail> details = new LinkedHashSet<>();



@CollectionTable의 joinColumns 속성을 사용해서 CHECK_D에서 CHECK_H를 참조할 때 사용하는 조인 칼럼을 지정했다. Check가 필요한 기능에서 CheckDetail도 함께 사용하기에 @ElementCollection의 fetch 속성을 EAGER로 설정했다. 화면에서 GROUP과 ITEM을 오름차순 기준으로 정렬해서 보여주기 때문에, 하이버네이트의 @OrderBy 애노테이션을 사용해서 값을 정렬했다.



Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

현재 참여하고 있는 프로젝트는 레거시 시스템과 관련이 있다. CHAR 타입 칼럼의 기본값으로 공백문자(' ')를 지정한 테이블이 많아서 기존 코드를 보면 TRIM 처리를 하는 쿼리가 많이 있다. 현재 프로젝트는 JPA를 사용하고 있는데, 조회 결과에서 불필요한 공백을 제거할 필요가 생겼다. 이를 위해 EntityListener와 커스텀 애노테이션을 사용했다.


@Trim 커스텀 애노테이션


DB에서 읽어온 값을 trim해야 하는 대상을 설정하기 위해 @Trim 애노테이션을 추가했다.


@Target({ ElementType.FIELD })

@Retention(RUNTIME)

public @interface Trim {

}


JPA 엔티티 리스너(entity listener) 메서드 구현


엔티티 단위로 @Trim 애노테이션이 붙은 필드에 대해 trim 처리를 수행하기 위해 JPA 엔티티 리스너 메서드를 구현했다. 엔티티 리스너 메서드는 크게 별도 클래스로 구현하거나 엔티티 메서드로 구현하는 두 가지 방법이 존재하는데 범용적으로 적용하기 위해 별도 클래스로 구현했다. 구현 클래스는 아래와 같다.


import javax.persistence.PostLoad;


public class TrimEntityListener {


    @PostLoad

    public void postLoad(Object entity) {

        Field[] fields = entity.getClass().getDeclaredFields();

        for (Field field : fields) {

            if (field.isAnnotationPresent(Trim.class)) {

                field.setAccessible(true);

                try {

                    Object value = field.get(entity);

                    if (value != null && value instanceof String) {

                        String s = (String) value;

                        field.set(entity, s.trim());

                    }

                } catch (Exception ex) {

                }

            }

        }

    }

}


@PostLoad 애노테이션은 엔티티를 로딩한 후에 호출할 메서드를 설정한다. @PostLoad 외에도 엔티티 라이프사이클에 맞춘 @PrePersist, @PostPersist, @PreUpdate, @PostUpdate 등의 애노테이션이 존재한다.


postLoad() 메서드의 entity 파라미터를 로딩한 엔티티 객체이다. postLoad() 메서드는 엔티티 객체의 필드 중에서 @Trim 애노테이션이 존재하는 필드를 찾아서 값에 대해 trim 처리를 한다.


엔티티 리스너 클래스 적용


이제 엔티티 리스너 클래스를 사용하도록 설정만 하면 된다. 전체 엔티티를 대상으로 엔티티 리스너 클래스를 적용하고 싶다면 JPA 설정 파일(스프링 부트를 사용한다면 META-INF/orm.xml 파일)의 <entity-listener> 태그에 엔티티 리스너 클래스를 설정하면 된다.


<?xml version="1.0" encoding="UTF-8"?>


<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm

                    http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd"

  version="2.1">


  <persistence-unit-metadata>

    <persistence-unit-defaults>

      <entity-listeners>

        <entity-listener class="jpa.TrimEntityListener" />

      </entity-listeners>

    </persistence-unit-defaults>

  </persistence-unit-metadata>

  

</entity-mappings>


엔티티별로 엔티티 리스너 클래스를 적용할 수도 있다. @EntityListener 애노테이션을 사용해서 각 엔티티 클래스에 사용할 엔티티 리스너를 지정하면 된다.


import javax.persistence.EntityListeners;


@Entity

@EntityListeners({TrimEntityListener.class})

public class Division {


    @Trim

    private String location;

    ...

}


TrimEntityListener는 @PostLoad 메서드를 정의하고 있으므로, 엔티티를 로딩할 때마다 @PostLoad 메서드를 사용해서 @Trim이 붙은 필드 값을 trim 처리할 수 있다.

Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링부터 다중 데이터소스 설정 방법은 간단하다. 먼저 application.properties 파일에 "구분.datasource"로 시작하는 데이터소스 설정을 추가한다. 다음은 예이다.



# datasource 1

spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

spring.datasource.url=jdbc:oracle:thin:@10.xx.xx.xx:SID

spring.datasource.username=dbuser

spring.datasource.password=dbpw


spring.datasource.tomcat.initialSize=5

spring.datasource.tomcat.maxActive=40

spring.datasource.tomcat.maxIdle=40

spring.datasource.tomcat.minIdle=5

spring.datasource.tomcat.maxWait=5

spring.datasource.tomcat.validationQuery=select 1+1 from dual

spring.datasource.tomcat.testWhileIdle=true

spring.datasource.tomcat.timeBetweenEvictionRunsMillis=60000


# datasource 2

sms.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver

sms.datasource.url=jdbc:sqlserver://10.xx.xx.xx:1433;databaseName=SMS

sms.datasource.username=dbuser1

sms.datasource.password=dbpw1


sms.datasource.tomcat.initialSize=1

sms.datasource.tomcat.maxActive=20

sms.datasource.tomcat.maxIdle=20

sms.datasource.tomcat.minIdle=1

sms.datasource.tomcat.maxWait=1

sms.datasource.tomcat.validationQuery=select 1

sms.datasource.tomcat.testWhileIdle=true

sms.datasource.tomcat.timeBetweenEvictionRunsMillis=60000


이 코드는 spring.datasource로 시작하는 데이터소스와 sms.datasource로 시작하는 데이터소스를 설정했다. "구분.datasource.tomcat"으로 시작하는 설정은 톰캣 DBCP 설정이다.


스프링부트는 별도 설정을 하지 않으면 spring.datasource로 시작하는 설정만 데이터소스로 사용하므로, 다중 데이터소스를 사용하려면 별도 설정을 추가해야 한다. 다음은 다중 데이터소스를 위한 자바 설정 예이다.


@Configuration

public class DataSourceConfiguration {


    @Bean

    @ConfigurationProperties(prefix = "spring.datasource")

    public DataSourceProperties dataSourceProp() {

        return new DataSourceProperties();

    }


    @Primary

    @Bean(name = "dataSource")

    @ConfigurationProperties(prefix = "spring.datasource.tomcat")

    @Qualifier("primary")

    public DataSource dataSource() {

        return dataSourceProp().initializeDataSourceBuilder().build();

    }


    @Primary

    @Bean(name = "transactionManager")

    @Qualifier("primary")

    public PlatformTransactionManager transactionManager() {

        return new DataSourceTransactionManager(dataSource());

    }


    @Bean

    @ConfigurationProperties(prefix = "sms.datasource")

    public DataSourceProperties smsDataSourceProp() {

        return new DataSourceProperties();

    }


    @Bean(name = "smsDataSource")

    @ConfigurationProperties(prefix = "sms.datasource.tomcat")

    @Qualifier("sms")

    public DataSource smsDataSource() {

        return smsDataSourceProp().initializeDataSourceBuilder().build();

    }


    @Bean(name = "smsTransactionManager")

    @Qualifier("sms")

    public PlatformTransactionManager smsTransactionManager() {

        return new DataSourceTransactionManager(smsDataSource());

    }



각 데이터소스 설정별로 세 개의 빈을 설정했다.

  • 설정 프로퍼티를 담은 DataSourceProperties 빈
    • @ConfigurationProperties를 사용해서 접두어를 지정(예, smsDataSourceProp() 빈 설정은 @ConfigurationProperties의 prefix 값으로 sms.datasource 사용)
  • DataSourceProperties 빈을 이용해서 DataSource 빈 생성
    • DataSourceProperties의 initializeDataSourceBuilder() 메서드를 이용해서 생성해야 tomcat 설정이 적용
  • 각 DataSource 마다 트랜잭션관리자 생성
    • 글로벌 트랜잭션을 사용해야 하면 JTA 트랜잭션 설정


Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링의 @EnableCache 설정과 @Cacheable 설정을 사용하면 매우 쉽게 레디시를 캐시로 사용할 수 있어서 편리하다. 한 가지 불편한 점이 있다면 캐시키를 JDK 직렬화를 사용해서 저장한다는 것이다. 그러다보니 상상한 레디스 키가 cache:1:2:3" 문자열이어도(캐시 이름 cache, 키 "1:2:3") 실제 레디스에 들어간 키는 "cache:타입데이터1:2:3" 이런 모양이 된다. 이는 레디스에 직접 연결해서 데이터를 확인할 때 불편함을 준다.


JDK 직렬화 대신 문자열로 키를 생성하는 KeySerializer를 사용하면 이 단점을 보완할 수 있다. 이를 하려면 직접 RedisTemplate을 생성해주면 된다. 다음은 코드 구현 예이다.


import java.net.UnknownHostException;


import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;

import org.springframework.data.redis.connection.RedisConnectionFactory;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.serializer.StringRedisSerializer;


@Configuration

public class RedisTemplateConfiguration {


    @Bean

    @Primary

    public RedisTemplate<Object, Object> redisTemplate(

            RedisConnectionFactory redisConnectionFactory)

            throws UnknownHostException {

        RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();

        template.setConnectionFactory(redisConnectionFactory);

        template.setKeySerializer(new StringRedisSerializer());

        return template;

    }

}


StringRedisSerializer는 키 값을 직렬화할 때 String.getBytes()를 사용하므로 JDK 직렬화를 사용할 때처럼 불필요한 타입정보가 붙지 않는다.

Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링은 캐시 구현으로 레디스를 지원한다. 스프링 부트를 사용하면 다음의 간단한 설정만 추가하면 된다.


<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-cache</artifactId>

</dependency>


이 설정을 추가하면 @EnableCaching 만으로 쉽게 @Cacheable과 관련 애노테이션을 사용해서 레디스를 캐시로 사용할 수 있다.

그런데, 스프링 부트가 제공하는 설정만으로 레디스를 캐시로 사용하면 캐시별로 유효 시간을 줄 수 없다. Ehcache를 캐시로 사용하면 설정 파일을 이용해서 유효 시간을 줄 수 있는데 레디스의 경우 설정 파일도 없다.(http://docs.spring.io/spring-boot/docs/1.4.3.RELEASE/reference/html/common-application-properties.html 참고)


진행중인 프로젝트에 캐시별 유효시간 설정 기능이 필요해서 스프링 부트로 다음과 같은 프로퍼티를 사용해서 캐시별로 유효 시간을 설정할 수 있도록 구현해봤다.


# application.properties

spring.cache.redis.defaultExpireTime=0

spring.cache.redis.expireTime.billDetailData=3600

spring.cache.redis.expireTime.billSummaryInfos=3600


이 설정에서 "spring.cache.redis"는 접두어이다. defaultExpireTime은 전체 캐시에 기본으로 적용할 유효시간을 설정한다. "expirTime.캐시이름"은 캐시 이름별로 캐시 시간을 설정한다. 유효 시간은 초 단위이다.


이 설정을 담기 위한 @ConfigurationProperties 클래스를 프로퍼티 클래스를 다음과 같이 작성했다.


import java.util.HashMap;

import java.util.Map;

import java.util.Map.Entry;


import org.springframework.boot.context.properties.ConfigurationProperties;


@ConfigurationProperties(prefix = "spring.cache.redis")

public class CacheRedisProperties {


    private long defaultExpireTime = 0L;

    private Map<String, Long> expireTime = new HashMap<>();


    private CacheTimeParser parser = new CacheTimeParser();


    public long getDefaultExpireTime() {

        return defaultExpireTime;

    }


    public void setDefaultExpireTime(long defaultExpireTime) {

        this.defaultExpireTime = defaultExpireTime;

    }


    public Map<String, Long> getExpireTime() {

        return expireTime;

    }


    public void setExpireTime(Map<String, Long> expireTime) {

        this.expireTime = expireTime;

    }

}


다음으로 RedisCacheManager에 유효 시간 설정하면 된다. 이를 위한 코드는 다음과 같다.


import java.util.Map.Entry;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;

import org.springframework.boot.context.properties.EnableConfigurationProperties;

import org.springframework.cache.annotation.EnableCaching;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Profile;

import org.springframework.data.redis.cache.RedisCacheManager;


/**

 * org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 클래스 참고

 */

@Configuration

@EnableCaching

@EnableConfigurationProperties(CacheRedisProperties.class)

public class CustomRedisCacheConfiguration {

    private Logger logger = LoggerFactory.getLogger(getClass());


    @Autowired

    private CacheRedisProperties cacheRedisProperties;


    @Bean

    public CacheManagerCustomizer<RedisCacheManager> cacheManagerCustomizer() {

        return new CacheManagerCustomizer<RedisCacheManager>() {

            @Override

            public void customize(RedisCacheManager cacheManager) {

                cacheManager.setDefaultExpiration(cacheRedisProperties.getDefaultExpireTime());

                cacheManager.setExpires(cacheRedisProperties.getExpireTime());

            }

        };

    }

}


스프링부트가 제공하는 CacheManagerCustomizer를 이용하면 부트가 생성한 CacheManager를 커스터마이징할 수 있다. 이 기능을 사용해서 RedisCacheManager에 캐시 유효 시간을 설정하면 된다.


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. ㅇㅇ 2017.01.29 20:55 신고  댓글주소  수정/삭제  댓글쓰기

    최범균님이 집필하신 책으로 공부하고 있습니다.

    저에게 많은 도움이 되는 것 같아 감사의 의미로 댓글 남깁니다.

    새해 복 많이 받으시고 건강하세요. ^^

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

다음과 같은 간단한 이벤트 관련 코드를 만들 일이 생겼다.

  1. 도메인 객체가 트랜잭션 범위에서 이벤트를 발생하면 핸들러로 처리
  2. 트랜잭션이 커밋된 이후에 이벤트 핸들러에 이벤트 전달해야 함
  3. 이벤트가 유실되어 처리하지 못해도 됨(실패시 후처리)
  4. 이벤트 핸들러는 비동기로 실행

스프링 4.2나 그 이후 버전을 사용한다면 아주 간단하게 위 조건을 충족하는 코드를 만들 수 있다. 다음 조합을 사용하면 된다.

  • ApplicationEventPublisher.publishEvent(Object event) 사용
  • @TransactionEventListener
  • @Async로 비동기 처리


1. Events 클래스

다음은 도메인 객체에서 이벤트를 발생시킬 때 사용할 Events 클래스이다.


import org.springframework.context.ApplicationEventPublisher;


public class Events {

    private static ThreadLocal<ApplicationEventPublisher> publisherLocal = 

            new ThreadLocal<>();


    public static void raise(DomainEvent event) {

        if (event == null) return;


        if (publisherLocal.get() != null) {

            publisherLocal.get().publishEvent(event);

        }

    }


    static void setPublisher(ApplicationEventPublisher publisher) {

        publisherLocal.set(publisher);

    }


    static void reset() {

        publisherLocal.remove();

    }

}


Events 클래스의 raise() 메서드는 ApplicationEventPublisher를 이용해서 이벤트를 퍼블리싱한다. 참고로 raise() 메서드의 event 파라미터는 Event 타입인데 이 타입은 원하는 타입으로 알맞게 만들면 된다.


도메인 객체에서 Events.raise()로 발생한 이벤트를 ApplicationEventPublisher로 퍼블리싱하려면 도메인 객체를 실행하기 전에 Events.setPublisher()로 ApplicationEventPublisher를 설정해야 한다. 이를 위해 다음의 Aspect를 구현했다..


import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.springframework.context.ApplicationEventPublisher;

import org.springframework.context.ApplicationEventPublisherAware;

import org.springframework.stereotype.Component;


@Aspect

@Component

public class EventPublisherAspect implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher publisher;

    private ThreadLocal<Boolean> appliedLocal = new ThreadLocal<>();


    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")

    public Object handleEvent(ProceedingJoinPoint joinPoint) throws Throwable {

        Boolean appliedValue = appliedLocal.get();

        boolean nested = false;

        if (appliedValue != null && appliedValue) {

            nested = true;

        } else {

            nested = false;

            appliedLocal.set(Boolean.TRUE);

        }

        if (!nested) Events.setPublisher(publisher);

        try {

            return joinPoint.proceed();

        } finally {

            if (!nested) {

                Events.reset();

                appliedLocal.remove();

            }

        }

    }


    @Override

    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {

        this.publisher = eventPublisher;

    }

}


트랜잭션 범위에서 실행되는 경우에만 이벤트를 처리하기 위해 @Transactional을 적용한 경우에만 적용하도록 설정했다. 대상 메서드를 실행하기 전에 Events.setPublisher()를 이용해서 스프링의 ApplicationEventPublisher를 설정하고, 대상 메서드를 실행한 후에 Events.reset()으로 초기화하도록 했다.


이제 트랜잭션 범위에서 실행되는 도메인 객체는 다음과 같은 코드를 이용해서 이벤트를 발생시키면 된다.


public class Order {


    public void cancel() {

        ...

        Events.raise(new OrderCanceledEvent(this.id));

    }

}



2. @TransactionEventListener로 이벤트 핸들러 구현하기

스프링 4.2 이전까지는 트랜잭션과 동기화해서 뭘 실행하려면 TransactionSynchronizationManager를 사용해야 했는데, 스프링 4.2에 들어간 @TransactionEventListener를 사용하면 손쉽게 트랜잭션 커밋 이후에 이벤트 핸들러를 실행할 수 있다.


import org.springframework.stereotype.Component;

import org.springframework.transaction.event.TransactionalEventListener;


@Component

public class EventHandler {


    @TransactionalEventListener

    public void handle(OrderCanceledEvent event) {

        // ... 이벤트 처리

    }



@TransactionalEventListener의 phase 속성을 사용하면 트랜잭션 커밋 이후뿐만 아니라 커밋 전, 롤백 이후, 커밋이나 롤백 이후에 이벤트를 처리하도록 설정할 수 있다.


트랜잭션 여부에 상관없이 이벤트 발생 시점에 이벤트를 처리하고 싶다면 @EventListener를 사용하면 된다.


3. @EnableAsync와 @Async로 비동기로 핸들러 실행하기

이벤트 핸들러를 비동기로 처리하고 싶다면 @EnableAsync와 @Async를 사용하면 된다. 스프링 설정 클래스에 @EnableAsync를 추가했다면, @TransactionalEventListener와 @Async를 함께 사용해서 이벤트를 트랜잭션 커밋 이후에 비동기로 처리할 수 있다.



import org.springframework.scheduling.annotation.Async;

import org.springframework.stereotype.Component;

import org.springframework.transaction.event.TransactionalEventListener;


@Component

public class EventHandler {


    @Async

    @TransactionalEventListener

    public void handle(OrderCanceledEvent event) {

        // ... 이벤트 처리

    }




Posted by 최범균 madvirus

댓글을 달아 주세요

  1. coding8282 2017.03.19 09:43 신고  댓글주소  수정/삭제  댓글쓰기

    저는 Domain Event Handler를 따로따로 만들어서 사용했었는데, 이 방법을 적용하니 정말 좋네요. 비동기 처리도 간단하구요~... 응용 범위가 굉장히 넓을 것 같습니다~~~ 감사합니다

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

내장 톰캣을 사용해서 동작하는 스프링부트 기반 웹어플리케이션을 AJP 프로토콜을 이용해서 아파치 HTTPD 웹 서버와 연동할 일이 생겼다. 연동을 위해 할 내용은 생각보다 간단했다. 다음의 두 가지만 해 주면 된다.

  1. 스프링부트 어플리케이션: 내장 톰캣을 위한 AJP 커넥터 설정
  2. 아파치 웹 서버 : ProxyPass로 ajp 연동 설정

스프링부트 내장 톰캣 설정


내장 톰캣 설정에 AJP 커넥터를 추가한다.


@Configuration

public class ContainerConfig {

    @Bean

    public EmbeddedServletContainerCustomizer containerCustomizer() {

        return container -> {

            TomcatEmbeddedServletContainerFactory tomcat

                    (TomcatEmbeddedServletContainerFactory) container;


            Connector ajpConnector = new Connector("AJP/1.3");

            ajpConnector.setProtocol("AJP/1.3");

            ajpConnector.setPort(9090);

            ajpConnector.setSecure(false);

            ajpConnector.setAllowTrace(false);

            ajpConnector.setScheme("http");

            tomcat.addAdditionalTomcatConnectors(ajpConnector);

        };

    }

}


아파치 설정


아파치에 톰캣 관련 설정을 추가한다.


ProxyPass "/contextPath" "ajp://localhost:9090/contextPath"


그리고 아파치 서버를 재시작하면 끝이다.

Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 임예준 2016.06.14 10:29 신고  댓글주소  수정/삭제  댓글쓰기

    이걸 몰라서 boot 내장 톰캣을 못쓰고,
    static 과 java 리소스를 httpd 와 외부 톰캣에 각각 배포 해야 되나 싶었는데 말입니다.
    ProxyPass를 사용하면 JK를 안써도 되는거죠?

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링 부트는 application.properties 파일을 이용해서 설정을 제공한다. 이 파일에는 부트가 제공하는 프로퍼티뿐만 아니라 커스텀 프로퍼티를 추가할 수 있다. 커스텀 프로퍼티를 사용하는 몇 가지 방법이 있는데 그 중에서 설정 프로퍼티 클래스를 사용하면 관련 설정을 한 클래스에서 관리할 수 있어 편리하다.


설정 프로퍼티 클래스를 사용하는 방법은 간단하다. 먼저, 설정 프로퍼티 클래스로 사용할 클래스를 작성한다. 이 클래스는 다음과 같이 작성한다.

  • @ConfigurationProperties 애노테이션을 클래스에 적용한다. application.properties 파일에서 사용할 접두어를 지정한다.
  • application.properties 파일에 설정한 프로퍼티 값을 전달받을 setter를 추가한다.
  • 프로퍼티 값을 참조할 때 사용할 get 메서드를 추가한다.
다음은 설정 프로퍼티 클래스의 작성 예이다.


@ConfigurationProperties(prefix = "eval.security")

public class SecuritySetting {

    private String authcookie;

    private String authcookieSalt;


    public String getAuthcookie() {

        return authcookie;

    }


    public void setAuthcookie(String authcookie) {

        this.authcookie = authcookie;

    }


    public String getAuthcookieSalt() {

        return authcookieSalt;

    }


    public void setAuthcookieSalt(String authcookieSalt) {

        this.authcookieSalt = authcookieSalt;

    }


}


application.properties 파일의 프로퍼티의 설정 프로퍼티 클래스의 프로퍼티는 다음과 같이 매칭된다.


* application.properties 프로퍼티 이름 = prefix + "." + setter 프로퍼티 이름


예를 들어, 위 코드에서 prefix는 "eval.security"이므로 setAuthcookie()에 해당하는 프로퍼티는 "eval.security.authcookie"가 된다. 만약 setter의 프로퍼티 이름 중간에 대문자가 포함되어 있다면 다양한 매핑을 지원한다. 예를 들어, 위 코드에서 authcookieSalt가 중간에 대문자를 포함하고 있는데 이 경우 다음과 같은 프로퍼티로부터 값을 가져올 수 있다.

  • eval.security.authcookieSalt
  • eval.security.authcookie-salt
  • eval.security.authcookie_salt
  • EVAL_SECURITY_AUTHCOOKIE_SALT

@ConfigurationProperties를 적용한 클래스를 만들었다면, 다음 할 일은 빈으로 등록하는 것이다. 스프링 빈으로 등록하는 방법은 간단하다. 다음의 두 가지 방식 중 하나를 사용하면 된다.

  • @EnableConfigurationProperties을 이용해서 지정하기
  • 설정 프로퍼티 클래스에 @Configuration 적용하기 (또는 설정 프로퍼티 클래스를 @Bean으로 등록하기)

먼저 @EnableConfigurationProperties을 사용하는 방법은 다음과 같다. @EnableConfigurationProperties을 설정 클래스에 추가하고 @EnableConfigurationProperties의 값으로 @ConfigurationProperties를 적용한 클래스를 지정하면 된다. 이 경우 @EnableConfigurationProperties는 해당 클래스를 빈으로 등록하고 프로퍼티 값을 할당한다.


@SpringBootApplication

@EnableConfigurationProperties(SecuritySetting.class)

public class Application { ... }


두 번째 방법은 설정 프로퍼티 클래스를 빈으로 등록하는 것이다. 스프링 부트는 컴포넌트 스캔을 하므로 설정 프로퍼티 클래스에 @Configuration을 붙이면 자동으로 빈으로 등록된다. 스프링 부트는 해당 빈이 @ConfigurationProperties를 적용한 경우 프로퍼티 값을 할당한다.


@ConfigurationProperties 적용 클래스를 빈으로 등록했다면 이제 설정 정보가 필요한 곳에서 해당 빈을 주입받아 사용하면 된다. 예를 들면 다음과 같이 자동 주입 받아 필요한 정보를 사용하면 된다.


@Configuration

public class SecurityConfig {

    @Autowired

    private SecuritySetting securitySetting;


    @Bean

    public Encryptor encryptor() {

        Encryptor encryptor = new Encryptor();

        encryptor.setSalt(securitySetting.getAuthcookieSalt());

        ...

    }


@ConfigurationProperties를 사용할 때의 장점은 다음과 같다.

  • 필요한 외부 설정을 접두어(prefix)로 묶을 수 있고 중첩 설정을 지원한다. (예, YAML을 사용하면 계층 구조로 묶을 수 있다.)
  • 기본 값을 쉽게 지정할 수 있다. 설정 프로퍼티 클래스의 필드에 기본 값을 주면 된다.
  • int, double 등 String 이외의 타입을 설정 프로퍼티 클래스의 프로퍼티에 사용할 수 있다. 설정 파일의 문자열을 설정 프로퍼티 클래스의 프로퍼티 타입으로 스프링이 알아서 변환해준다.



Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링을 이용해서 회사 서비스를 개발하다보면 은근히 비슷하게 작성하는 설정 코드가 존재하기 마련이다. 예를 들어, 여러 서비스에서 LDAP 연동을 위해 같은 스프링 시큐리티 설정을 사용하거나 내부 API 연동을 위해 동일한 설정을 사용할 수 있다. 또한, 데이터 저장소에 따라 사내에서 제안하는 설정 가이드가 존재할 수도 있다.


이런 류의 설정은 여러 곳에서 반복되기 때문에 http://javacan.tistory.com/entry/spring-at-enable-config 글에서 설명한 @Enable을 이용해서 커스텀 설정 편의 기능을 제공하면 편리하다. 스프링부트를 사용한다면, 부트의 자동 설정 기능까지 함께 지원함으로써 설정 편의 기능을 완성할 수 있다.


스프링캠프2016에서 이수홍님이 발표하신 자료(http://www.slideshare.net/sbcoba/2016-deep-dive-into-spring-boot-autoconfiguration-61584342)를 보면 스프링 부트의 자동 설정 기능에 대해 많은 내용을 알 수 있다. 이 자료와 함께 이수홍님 깃헙 프로젝트(https://github.com/sbcoba/spring-camp-2016-spring-boot-autoconfiguration)에서 관련 코드도 참고할 수 있다.


이수홍님 자료를 바탕으로 자동 설정 기능을 추가하는 방법을 정리하면 다음과 같다.

  • 자동 설정 기능 제공 모듈: 설정을 제공하는 @Configuration 적용 클래스 구현
  • 자동 설정 기능 제공 모듈: spring.factories 파일 작성
  • 적용: 자동 설정이 필요한 프로젝트에서 모듈에 대한 의존 추가

1. 자동으로 설정할 내용을 담을 @Configuration 클래스 작성


자동 설정 기능을 위한 코드를 담고 있는 모듈(jar 파일)은 @Configuration 애노테이션을 사용해서 설정 클래스를 작성한다.


@Configuration

@ConditionalOnMissingBean(AuthClient.class)

public class CompAutoConf {


    @Bean

    public AuthClient authClient() {

        return new HttpAuthClient();

    }



이 설정 클래스는 일반적인 스프링 설정 클래스와 차이가 없다. 보통 자동 설정 기능에서 사용하는 @Configuration 클래스는 조건에 따라 설정 사용 여부를 결정하기 위해 @Conditional 애노테이션을 포함한다. 위 코드의 경우 AuthClient 타입의 빈이 없는 경우에만 CompAutoConf를 설정 클래스로 사용하도록 했다.



2. META-INF/spring.factories 파일에 설정 클래스 지정하기


설정 클래스를 구현했다면, 그 다음으로 할 일은 spring.factories 파일에 설정 클래스를 지정하는 것이다. 클래스패스 위치에(메이븐 같으면 src/main/resources 폴더에) META-INF/spring.factories 파일을 만들고, org.springframework.boot.autoconfigure.EnableAutoConfiguration 프로퍼티의 값으로 자동 설정으로 사용할 클래스를 값으로 준다.


# Auto Configure

org.springframework.boot.autoconfigure.EnableAutoConfiguration=mycompany.CompAutoConf



3. 자동 설정 모듈 사용


앞서 언급한 이수홍님 자료를 보면, 스프링 부트는 클래스패스에 위치한 모든 META-INF/spring.factories 파일의 org.springframework.boot.autoconfigure.EnableAutoConfiguration 프로퍼티 값을 읽어와 설정 클래스로 사용한다. 따라서, 스프링 부트 프로젝트에 자동 설정 모듈에 대한 의존을 추가해주기만 하면 된다.


@SpringBootApplication // META-INF/spring.factories에 지정한 설정 클래스 사용

public class Application {


    public static void main(String[] args) {

        ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args);

        // CompAutoConf에 정의한 @Bean 설정에 따라 HttpAuthClient 객체를 빈으로 등록

        AuthClient client = ctx.getBean(AuthClient.class);

        ...

    }

}



조건에 따라 설정 적용하기: @Conditional


앞서 CompAutoConf 예를 보면 @ConditiionalOnMissingBean(AuthClient.class)가 있는데, 이는 AuthClient 타입의 빈이 없는 경우에만 해당 설정을 사용한다는 것을 의미한다. 만약 다음과 같이 설정에 AuthClient 타입 빈이 포함되어 있으면 @ConditiionalOnMissingBean(AuthClient.class)에 따라 CompAutoConf 설정 클래스를 사용하지 않는다.


@SpringBootApplication

public class Application {


    @Bean

    public AuthClient authClient() {

        return new ProtobuffAuthClient();

    }


    ...


@Conditional은 스프링 4.0부터 지원하는데, 스프링은 클래스 존재 여부, 프로퍼티 존재 여부, 빈 존재 여부 등 다양한 @Conditional을 제공하고 있다.


설정 순서 지정하기: @AutoConfigureBefore/@AutoConfigureAfter/@AutoConfigureOrder


자동 설정 클래스를 적용하는 순서가 중요할 때가 있다. 예를 들어, 내가 만들 자동 설정이 DataSource를 설정한다고 해보자. 이 자동 설정은 스프링 부트가 제공하는 DataSource 자동 설정 클래스인 DataSourceAutoConfiguration보다 먼저 실행되어야 한다. 그렇지 않을 경우, DataSourceAutoConfiguration이 생성한 DataSource와 내가 만든 자동 설정 기능이 생성한 DataSource가 함께 만들어진다.


보통 이는 원하는 결과가 아니다. 스프링 부트의 DataSourceAutoConfiguration는 DataSource 타입 빈이 존재하지 않는 경우에만 DataSource를 만드므로, 스프링 부트의 DataSourceAutoConfiguration보다 내가 만든 자동 설정 클래스가 먼저 실행되면 여러 DataSource가 만들어지는 문제를 피할 수 있다. 이를 위한 간단한 방법은 내가 만든 자동 설정 클래스에 @AutoConfigureBefore를 붙이는 것이다.


@AutoConfigureBefore(DataSourceAutoConfiguration.class)

@ConditionalOnMissingBean(DataSource.class)

public class MyCompDataSourceAutoConfiguration {

    ...

}


@AutoConfigureAfter를 사용해서 다른 설정 뒤에 자동 설정을 적용하거나 @AutoConfigureOrder를 사용해서 설정의 우선순위를 지정할 수 있다.

Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링 3.0 버전까지만 해도 스프링 자바 설정이 상대적으로 인기가 없었는데, 주된 이유는 XML의 네임스페이스 기반 설정 편의 기능을 대체하지 못했기 때문이었다. 스프링 3.1 버전부터 자바 설정도 편의 기능을 제공하기 시작했는데 그것이 바로 @Enable로 시작하는 애노테이션 설정이다. @Enable 설정 편의 애노테이션은 사용자를 대신해서 많은 설정을 대신 해준다. 예를 들어, @EnableWebMvc의 경우 100여 줄에 가까운 설정 코드륻 대신 한다. 


스프링이 제공하는 @Enable 설정 편의 기능뿐만 아니라 직접 @Enable 설정 편의 애노테이션을 만들 수도 있는데, 크게 세 가지 방법이 존재한다.


1. 설정을 임포트하는 @Enable 애노테이션


첫 번째 방법은 @Configuration 설정을 임포트하는 @Enable 애노테이션을 작성하는 것이다. 예를 들어, 다음 @Configuration 설정을 보자.


@Configuration

public class MyCompanyConfig {

    

    @Bean

    public Authenticator authenticator() {

        Authenticator authenticator = new Authenticator();

        ...

        return authenticator;

    }


}


이 설정을 자동으로 처리하는 @Enable 애노테이션은 다음과 같이 구현할 수 있다.


@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

@Import(MyCompanyConfig.class)

@interface EnableMyCompany {}


@Import 속성은 설정으로 사용할 클래스를 지정한다.


이제 @EnableMyCompany 애노테이션을 설정에 추가하면 MyCompanyConfig에 포함된 설정을 자동으로 포함시킨다.


@Configuration

@EnableMyCompany

public class AppConfig {


    @Autowired

    private Authenticator authenticator;


    ...

}



2. ImportSelector 사용하기


두 번째 방법은 ImportSelector를 사용하는 것이다. 애노테이션 애트리뷰트 값에 따라 다른 설정 클래스를 사용하고 싶을 때 이 방식을 사용한다. 예를 들어, 다음 @Enable 애노테이션을 보자.


@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

@Import(CompanyConfigSelector.class)

public @interface EnableAuthConfig {

    ClientType type() default ClientType.HTTP;

}


이 애노테이션은 type 애트리뷰트를 갖고 있다.


@Import에 지정한 CompanyConfigSelector는 ImportSelector 인터페이스를 구현한 클래스로서 다음 코드처럼 @EnableAuthConfig 애노테이션의 type 애트리뷰트 값에 따라 사용할 설정 클래스 이름을 리턴하도록 구현한다. ImportSelector 자체는 설정 클래스가 아니므로 @Configuration이 붙지 않는다.


public class CompanyConfigSelector implements ImportSelector {

    @Override

    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        Map<String, Object> attributesMap = 

                 importingClassMetadata.getAnnotationAttributes(

                         EnableAuthConfig.class.getName(), false);

        AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributesMap);

        ClientType typeValue = attributes.<ClientType>getEnum("type");

        if (typeValue == ClientType.HTTP) {

            return new String[]{HttpAuthClientConfiig.class.getName()};

        } else if (typeValue == ClientType.PROTOBUFF) {

            return new String[]{ProtobuffAuthClientConfig.class.getName()};

        }

        return new String[0];

    }

}



ImportSelector의 selectImport() 메서드는 String 배열을 리턴하는데, 이 배열은 설정으로 사용할 클래스 이름을 값으로 갖는다. 위 코드의 경우 @EnableAuthConfig 애노테이션의 type 애트리뷰트 값에 따라 HttpAuthClientConfig나 ProtobuffAuthClient를 설정으로 사용하도록 구현하고 있다. 이 두 클래스는 다음과 같이 @Configuration을 이용한 설정 클래스이다.


@Configuration

public class HttpAuthClientConfiig {

    @Bean

    public HttpAuthClient authClient() {

        return new HttpAuthClient();

    }

}


이제 @EnableAuthConfig 애노테이션을 사용하면 type 애트리뷰트 값에 따라 알맞은 설정을 사용하게 된다. 예를 들어, 아래 코드는 ProtobuffAuthClientConfig를 설정으로 사용하게 된다.


@Configuration

@EnableAuthConfig(type = ClientType.PROTOBUFF)

public class AppConfig {}



3. ImportBeanDefinitionRegistrar 사용하기


세 번째 방법은 ImportBeanDefinitionRegistrar을 사용하는 것이다. ImportSelector가 설정으로 사용할 클래스 이름을 리턴하는 방식이라면 ImportBeanDefinitionRegistrar는 빈 설정을 직접 등록하는 방식이다. 이 방식은 기존에 XML 스키마 확장 방식을 자바 설정으로 마이그레이션하고자 할 때 사용할 때 좋다.


Enable 애노테이션은 동일하다. 다음과 같이 @Import로 ImportBeanDefinitionRegistrar 인터페이스를 구현한 클래스를 지정하면 된다.


@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

@Import(CompanyConfigRegistrar.class)

public @interface EnableCompSecConfig {

    ClientType type() default ClientType.HTTP;

}


ImportBeanDefinitionRegistrar 인터페이스를 구현한 클래스는 애노테이션 애트리뷰트 정보를 이용해서 알맞은 빈 설정을 등록하면 된다. 다음은 간단한 구현 예이다.


public class CompanyConfigRegistrar implements ImportBeanDefinitionRegistrar {

    @Override

    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,

            BeanDefinitionRegistry registry) {

        Map<String, Object> attributesMap = 

                importingClassMetadata.getAnnotationAttributes(

                        EnableCompSecConfig.class.getName(), false);

        AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributesMap);

        ClientType typeValue = attributes.<ClientType>getEnum("type");

        if (typeValue == ClientType.HTTP) {

            BeanDefinition beanDef = new RootBeanDefinition("comp.config.HttpAuthClient");

            registry.registerBeanDefinition("authClient", beanDef);

        } else if (typeValue == ClientType.PROTOBUFF) {

            BeanDefinition beanDef = 

                    new RootBeanDefinition("comp.config.ProtobuffAuthClientConfiig");

            registry.registerBeanDefinition("authClient", beanDef);

        }

    }

}



이제 @EnableCompSecConfig를 사용하면 CompanyConfigRegistrar에서 등록한 빈 설정이 자등으로 추가된다.


@Configuration

@EnableCompSecConfig(type = ClientType.HTTP)

public class AppConfig {



Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

최근에 특정 개발 방식을 시도할 겸, 몇 가지 구현 기술도 익힐 겸해서 회사에서 필요한 어플리케이션을 만들어보고 있다. 스프링 부트와 자바, 스칼라, JPA, AngularJS 등을 사용하고 있고, 뷰 템플릿으로는 JSP를 이용했다.


배포의 단순함을 위해 스프링 부트의 실행가능한 war를 만든 뒤, 로컬PC와 운영될 서버에 올려서 기능 테스트를 진행했다. 테스트를 진행하는 과정에서 다음과 같은 증상을 발견했다.

  • 리눅스 서버에서 war를 실행: 동일한 JSP 뷰를 실행할 때, 약 4초 간격으로 응답 속도가 느려짐. 예를 들어, 새로고침을 짧은 간격으로 실행하면, 30~40ms 걸리던 응답 시간이 약 4초마다 300~400ms가 걸림
  • 윈도우 개발 PC에서 war를 실행: 증상 없음
  • 맥 개발 PC에서 war를 실행: 리눅스 서버와 동일 증상 발생
  • JSP를 사용하지 않는 JSON 응답의 경우 비슷한 증상 없음
  • mvn spring-boot:run으로 실행할 때에는 맥 개발 PC에서 이런 증상이 발생하지 않음
한 JSP 코드에서 <jsp:include>를 이용해서 다른 JSP를 include 할 경우, 응답 속도가 더 느려졌다. 몇 가지 의심되는 것이 있어서 차례대로 확인해 나가다보니 결과적으로 응답 속도를 느리게 만드는 용의자를 찾았다. 그 용의자는 톰캣 JSP 엔진 Jasper의 JSP 변경 여부 검사 옵션이었다.

톰캣의 JSP 엔진인 Jasper는 기본적으로 다음과 같이 설정되어 있다.
  • 개발모드: JSP의 변경 여부를 확인할지 여부 지정. 기본 값은 true.
  • 변경확인간격: 개발모드가 true인 경우, 지정한 간격이 지날 때 마다 실행하는 JSP의 변경을 확인. 기본 값은 4초.
4초! 그렇다. 4초다. 앞서 이상 증상에서 발견된 그 약 4초라는 시간이 설정에 있는 것이다. 원인은 잘 모르지만, 의심되는 것은 다음과 같다.
  • 실행 가능한 war로 묶인 경우, 리눅스 기반 OS의 JDK 사용시 war 파일에 포함된 파일의 변경 여부를 확인하는 과정에서 실행 속도가 느려짐
자바8로만 해봤기 때문에 자바7이나 자바6에서 동일 증상이 발생하는지 여부는 모르지만, 어쨋든 war 파일에 포함된 파일을 탐색하는 과정에서 속도가 느려지는 것 같다. 왜 그런지 알고 싶지만, 여기까지 파고 있을 시간이 없어서 다음 과정을 진행했다.

실행 가능 war의 경우 JSP 변경 여부를 확인하는 과정은 별 의미가 없기 때문에, 임베디드 톰캣이 JSP 변경 여부를 확인하지 않도록 스프링 설정을 변경했다. 이때 사용한 것이 TomcatContextCustomizer이다. 아래 코드는 Jasper의 개발모드를 false로 설정하기 위해 사용한 스프링 설정의 일부이다.

import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;

@Configuration
public class ContainerConfig {

    @Bean
    public ServerProperties serverProperties() {
        return new ServerProperties();
    }

    @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer(){
        return new TomcatContainerCustomizer(tomcatJspServletConfig());
    }

    @Bean
    public TomcatJspServletConfig tomcatJspServletConfig() {
        return new TomcatJspServletConfig();
    }

    static class TomcatContainerCustomizer implements EmbeddedServletContainerCustomizer {

        private TomcatJspServletConfig tomcatJspServletConfig;

        public TomcatContainerCustomizer(TomcatJspServletConfig tomcatJspServletConfig) {
            this.tomcatJspServletConfig = tomcatJspServletConfig;
        }

        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            ...
            if (container instanceof TomcatEmbeddedServletContainerFactory) {
                TomcatEmbeddedServletContainerFactory tomcatContainer = 
                      (TomcatEmbeddedServletContainerFactory) container;
                tomcatContainer.addContextCustomizers(tomcatJspServletConfig);
            }
        }

    }

    public static class TomcatJspServletConfig implements TomcatContextCustomizer {

        private ContainerJasperSetting jasperSetting;

        public TomcatJspServletConfig(ContainerJasperSetting jasperSetting) {
            this.jasperSetting = jasperSetting;
        }

        @Override
        public void customize(Context context) {
            // TomcatEmbeddedServletContainerFactory가 설정한 JspServlet의 이름이 "jsp"
            Wrapper jsp = (Wrapper)context.findChild("jsp");
            // JspServlet의 개발모드를 비활성화
            // 실제로는 application.properties 파일에 설정할 수 있는 프로퍼티 추가해서 구현
            jsp.addInitParameter("development", "false");
        }
    }

}

위 코드에서는 개발모드를 비활성화했지만, 실제 코드에서는 application.properties 파일에 "jsp.jasper.development" 프로퍼티의 값에 따라서 개발모드를 활성화/비활성화하도록 구현했다.

위와 같이 Jasper 엔진의 개발모드를 비활성화한 뒤에 실행가능한 war를 다시 실행해보니, 4초 간격으로 응답 시간이 느려지는 증상이 사라졌다.만세다!


Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.




Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.