springboot – Dreaming for the Future 영원한 개발자를 향해서. 월, 13 1월 2025 13:44:09 +0000 ko-KR hourly 1 https://wordpress.org/?v=4.7 108384747 Spring batch를 Parallel로 돌려보자 /index.php/2017/12/19/spring-batch-parallel-execution/ Mon, 18 Dec 2017 22:52:00 +0000 /?p=489

Continue reading ‘Spring batch를 Parallel로 돌려보자’ »]]> Monolithic 아키텍처 환경에서 가장 잘 돌아가는 어플리케이션 가운데 하나가 배치 작업이다. 모든 데이터와 처리 로직들이 한군데에 모여있기 때문에 최소한의 비용으로 빠르게 기능을 돌릴 수 있다. 데이터 존재하는 Big Database에 접근하거나 Super Application Server에 해당 기능의 수행을 요청하면 된다. 끝!!!

하지만 요즘의 우리가 개발하는 어플리케이션들은 R&R이 끝없이 분리된 Microservices 아키텍처의 세상에서 숨쉬고 있다. 배치가 실행될려면 이 서비스, 저 서비스에 접근해서 데이터를 얻어야 하고, 얻은 데이터를 다른 서비스의 api endpoint를 호출해서 최종적인 뭔가가 만들어지도록 해야한다.  문제는 시간이다!

마이크로서비스 환경에서 시간이 문제가 되는 요인은 여러가지가 있을 수 있다. 배치는 태생적으로 대용량의 데이터를 가지고 실행한다. 따라서 필요한 데이터를 획득하는게 관건이다. 이 데이터를 빠르게 획득할 수 없다면 배치의 실행 속도는 느려지게 된다. 다들 아는 바와 같이 마이크로서비스 환경이 일이 돌아가는 방식은 Big Logic의 실행이 아니라 여러 시스템으로 나뉘어진 Logic간의 Collaboration이다. 그리고 이 연동은 대부분 RESTful을 기반으로 이뤄진다.

RESTful이란 뭔가? HTTP(S) over TCP를 기반으로 한 웹 통신이다. 웹 통신의 특징은 Connectionless이다. (경우에 따라 Connection oriented) 방식이 있긴 하지만, 이건 아주 특수한 경우에나 해당한다. TCP 통신에서 가장 비용이 많이 들어가는 과정은 Connection setup 비용인데, RESTful api를 이용하는 과정에서는 API Call이 매번 발생할 때마다 계속 연결을 새로 맺어야 한다. (HTTP 헤더를 적절히 제어하면 이를 극복할 수도 있을 것 같지만 개발할 때 이를 일반적으로 적용하지는 않기 때문에 일단 스킵. 하지만 언제고 따로 공부해서 적용해봐야할 아젠다인 것 같기는 하다.)

따라서 Monolithic 환경과 같이 특정 데이터베이스들에 연결을 맺고, 이를 읽어들여 처리하는 방식과는 확연하게 대량 데이터를 처리할 때 명확하게 속도 저하가 발생한다. 그것도 아주 심각하게.

다시 말하지만 배치에서 속도는 생명이다. 그러나 개발자는 마이크로서비스를 사랑한다. 이 괴리를 맞출려면…

  1. 병렬처리를 극대화한다.
  2. 로직을 고쳐서 아예 데이터의 수를 줄인다.

근본적인 처방은 두번째 방법이지만, 시간이 별로 없다면 어쩔 수 없다. 병렬 처리로 실행하는 방법을 쓰는 수밖에…
병렬로 실해시키는 가장 간단한 방법은 ThreadPool이다. Springbatch에서 사용 가능한 TaskExecutor 가운데 병렬 처리를 가능하게 해주는 클래스들이 있다.

  • – 필요에 따라 쓰레드를 생성해서 사용하는 방식이다. 연습용이다. 대규모 병렬 처리에는 비추다.
  • – 쓰레드 제어를 위한 몇 가지 설정들을 제공한다. 대표적으로 풀을 구성하는 쓰레드의 개수를 정할 수 있다!!! 이외에도 실행되는 작업이 일정 시간 이상 걸렸을 때 이를 종료시킬 수 있는 기능들도 지원하지만… 이런 속성들의 경우에는 크게 쓸일은 없을 것 같다.

제대로 할려면 ThreadPoolTaskExecutor를 사용하는게 좋을 것 같다. 병렬 처리 가능한 TaskExecutor들은  인터페이스 페이지를 읽어보면 알 수 있다.

@Bean(name = "candidateTaskPool")
public TaskExecutor executor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(CORE_TASK_POOL_SIZE);
    executor.setMaxPoolSize(MAX_TASK_POOL_SIZE);
    return executor;
}

이 메소드 정의를 Job Configuration 객체에 반영하면 된다. ThreadPool을 생성할 때 한가지 팁은 Pool을 @Bean annotation을 이용해서 잡아두는게 훨씬 어플리케이션 운영상에 좋다. 작업을 할 때마다 풀을 다시 생성시키는 것이 Cost가 상당하니 말이다. 어떤 Pool이든 매번 만드는 건 어플리케이션 건강에 해롭다.

전체 배치 코드에 이 부분이 어떻게 녹아들어가는지는 아래 코드에서 볼 수 있다.

@Configuration
@EnableBatchProcessing
    public class CandidateJobConfig {
    public static final int CORE_TASK_POOL_SIZE = 24;
    public static final int MAX_TASK_POOL_SIZE = 128;
    public static final int CHUNK_AND_PAGE_SIZE = 400;

    @Bean(name = "candidateTaskPool")
    public TaskExecutor executor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_TASK_POOL_SIZE);
        executor.setMaxPoolSize(MAX_TASK_POOL_SIZE);
        return executor;
    }

    @Bean(name = "candidateStep")
    public Step step(StepBuilderFactory stepBuilderFactory,
                     ItemReader<User> reader,
                     ItemProcessor<User, Candidate> processor,
                     ItemWriter<Candidate> writer) {
        return stepBuilderFactory.get("candidateStep")
                                 .<User, Candidate>chunk(CHUNK_AND_PAGE_SIZE)
                                 .reader(candidateReader)
                                 .processor(candidateProcessor)
                                 .writer(candidateWriter)
                                 .taskExecutor(executor())
                                 .build();
    }

이렇게 하면 간단하다.

하지만 이게 다는 아니다. 이 코드는 다중 쓰레드를 가지고 작업을 병렬로 돌린다. 하지만 이 코드에는 한가지 문제점이 있다. 살펴보면 Chunk라는 단위로 작업이 실행된다는 것을 알 수 있다. Chunk는 데이터를 한개씩 읽는게 아니라 한꺼번에 여러 개(이 예제에서는 CHUNK_AND_PAGE_SIZE)씩 읽어 processor를 통해 실행한다. 배치의 실제 구현에 대한 이해나 고려가 필요하다.

Chunk를 사용해서 IO의 효율성을 높이는 방법은 흔하게 사용되는 방법이다. 하지만 입력 데이터를 Serialized된 형태로 읽어들여야 하는 경우라면 좀 더 고려가 필요하다. MultiThread 방식으로 배치가 실행되면 각 쓰레드들은 자신의 Chunk를 채우기 위해서 Reader를 호출한다. 만약 한번에 해당 Chunk가 채워지지 않으면 추가적인 데이터를 Reader에게 요청한다. 이 과정에서 쓰레드간 Race condition이 발생하고, 결국 읽는 과정에서 오류가 발생될 수 있다. 예를 들어 입력으로 단순 Stream Reader 혹은 RDBMS의 Cursor를 이용하는 경우에는.

문제가 된 케이스에서는 JDBCCursorItemReader를 써서 Reader를 구현하였다. 당연히 멀티 쓰레드 환경에 걸맞는 Synchronization이 없었기 때문에 Cursor의 내부 상태가 뒤죽박죽되어 Exception을 유발시켰다.

가장 간단한 해결 방법은 한번 읽어들일 때 Chunk의 크기와 동일한 크기의 데이터를 읽어들이도록 하는 방법이다. Tricky하지만 상당히 효율적인 방법이다.  Cursor가 DBMS Connection에 의존적이기 때문에 개별 쓰레드가 연결을 따로 맺어서 Cursor를 관리하는 것도 다른 방법일 수 있겠지만, 이러면 좀 많이 복잡해진다. ㅠㅠ 동일한 크기의 데이터를 읽어들이기 위해 Paging 방식으로 데이터를 읽어들일 수 있는 JDBCPagingItemReader 를 사용한다. 관련된 샘플은 다음 두개의 링크를 참고하면 쉽게 적용할 수 있다.

이걸 바탕으로 구현한 예제 Reader 코드는 아래와 같다. 이전에 작성한 코드는 이런 모양이다.

Before

public JdbcCursorItemReader<User> itemReader(DataSource auditDataSource,
                                                @Value("#{jobExecutionContext['oldDate']}") final Date oldDate) {
    JdbcCursorItemReader<User> reader = new JdbcCursorItemReader();
    String sql = "SELECT user_name, last_login_date FROM user WHERE last_login_date < '%s'";
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    reader.setSql(String.format(sql, sdf.format(elevenMonthAgoDate)));
    reader.setDataSource(auditDataSource);
    ...
}

After

public JdbcPagingItemReader<User> itemReader(DataSource auditDataSource,
                                             @Value("#{jobExecutionContext['oldDate']}") final Date oldDate) {
    JdbcPagingItemReader<User> reader = new JdbcPagingItemReader();

    SqlPagingQueryProviderFactoryBean factory = new SqlPagingQueryProviderFactoryBean();
    factory.setDataSource(auditDataSource);
    factory.setSelectClause("SELECT user_name, last_login_date ");
    factory.setFromClause("FROM user ");
    factory.setWhereClause(String.format("WHERE last_login_date < '%s'", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(oldDate)));
    factory.setSortKey("user_name");

    reader.setQueryProvider(factory.getObject());
    reader.setDataSource(auditDataSource);
    reader.setPageSize(CHUNK_AND_PAGE_SIZE);

    ...
}

여기에서 가장 핵심은 CHUNK_AND_PAGE_SIZE라는 Constant다. 이름에서 풍기는 의미를 대강 짐작하겠지만, Chunk에서 읽어들이는 값과 한 페이지에서 읽어들이는 개수가 같아야 한다는 것이다. 이러면 단일 Cursor를 사용하더라도 실행 Thread간의 경합 문제없이 간단히 문제를 잡을 수 있다. 하지만 명심할 건 이것도 뽀록이라는 사실.
이렇게 문제는 해결했지만 과연 마이크로서비스 환경의 배치로서 올바른 모습인가에 대해서는 의구심이 든다. 기존의 배치는 Monolithic 환경에서 개발되었고, 가급적 손을 덜 들이는 관점에서 접근하고 싶어서 쓰레드를 대량으로 투입해서 문제를 해결하긴 했다. 젠킨스를 활용하고, 별도의 어플리케이션 서버를 만들어서 구축한 시스템적인 접근 방법이 그닥 구린건 아니지만 태생적으로 다음의 문제점들이 있다는 생각이 작업중에 들었다.

  • SpringBatch가 기존의 주먹구구식 배치를 구조화시켜서 이쁘게 만든건 인정한다. 하지만 개별 서버의 한계를 넘어서지는 못했다.  한대의 장비라는 한계. 이게 문제다. 성능이 아무리 좋다고 하더라도 한대 장비에서 커버할 수 있는 동시 작업의 한계는 분명하다. 더구나 안쓰고 있는데도 장비가 물려있어야 하니 이것도 좀 낭비인 것 같기도 하고…
  • Microservice 환경의 Transaction cost는 기존 Monolithic에 비해 감내하기 힘든 Lagging effect를 유발시킨다. 그렇다고 개별 개별 서비스의 DB 혹은 Repository를 헤집으면서 데이터를 처리하는건 서비스간의 Indepedency를 유지해야한다는 철학과는 완전 상반된 짓이다. 구린 냄새를 풍기면서까지 이런 짓을 하고 싶지는 않다. Asynchronous하고, Parallelism을 충분히 지원할 수 있는 배치 구조가 필요하다.

한번에 다 할 수 없고, 일단 생각꺼리만 던져둔다. 화두를 던져두면 언젠가는 이야기할 수 있을거고, 재대로 된 방식을 직접 해보거나 대안제를 찾을 수 있겠지.

]]> 489
PathVariable에 Slash(/)가 값으로 처리하기 /index.php/2017/04/28/get-request-with-pathvariable-and-slash/ Thu, 27 Apr 2017 22:12:13 +0000 /?p=338

Continue reading ‘PathVariable에 Slash(/)가 값으로 처리하기’ »]]> RESTful 방식에서 URI는 Resource에 대한 접근을 어떤 방식으로 허용할지를 결정하는 중요한 요소이다.  당연히 특정 리소스의 구성 요소를 지정하는 방식으로 PathVariable을 사용해야한다. 대부분의 경우에는 별 문제없이 사용할 수 있지만 PathVariable에 특수문자가 들어오는 경우에 예상외의 오류가 발생하는 경우가 있다.  이런 대표적인 특수 문자가 Slash(/)이다.  Slash가 문제가 되는 이유는 짐작하겠지만, 이걸로 인해서 URI의 Path Separation이 발생하기 때문이다.

API Server Application

일반 설정으로는 Springboot 기반의 API 서버에서 Slash가 포함된 값을 PathVariable로 받을 수 없다.  Springboot에서는 기본적으로 /가 포함된 경우, 기본적으로 다음과 같은 정책을 적용한다.

  • Encoding된 Slash가 포함되었다 하더라도, 이를 Decode해서 변수 자체를 세부 Path로 구분해버린다.
  • 중복된 Slash가 존재하면 이를 합쳐서 하나의 Slash로 만들어버린다.

이걸 제대로 원래의 Original Value로 획득하기 위해서는 다음과 같은 옵션이 추가되어야 한다.

@Configuration
@EnableAutoConfiguration
@ComponentScan
@SpringBootApplication
public class ServiceApplication extends WebMvcConfigurerAdapter {
    ...
    public static void main(String[] args) throws Exception {
        System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
        SpringApplication.run(ServiceApplication.class, args);
    }

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        urlPathHelper.setUrlDecode(false);
        urlPathHelper.setAlwaysUseFullPath(true);
        configurer.setUrlPathHelper(urlPathHelper);
    }

코드에서 주의깊게 봐야 할 포인트는 두 가지다.

  • main() 메소드에서 SpringApplication 실행 전 단계에 ALLOW_ENCODE_SLASH 속성 값을 false로 만들어야 한다. Springboot의 Embeded Tomcat이 URI값을 임의로 decode하는 것을 차단하고, 이를 그 값 그대로 Spring 영역으로 보내도록 설정을 잡는다.  이걸 왜 굳이 System property에서 잡아야 하는지 애매하지만 톰캣과 스프링이 그리 친하지 않는걸로 일단 생각한다.
  • WebMvcConfigurerAdapter를 상속받아서 configurePathMatch() 메소드를 Overriding한다.  Overriding 메소드에서 UrlPathHelper 객체를 생성하여 다음의 두가지 속성을 추가로 false로 반영한다.
    • urlDecode – 스프링의 기본 URI filter에서 추가적인 decode를 수행하지 않는다. (그런데 이 옵션이 맞는지는 좀 까리하다. 앞단의 ALLOW_ENCODE_SLASH 만으로 충분한 것 같기도 하고…)
    • alwasyUseFullPath – // 경우에 자동으로 이걸 / 로 치환하도록 하는 규칙을 적용하지 않도록 한다.

이 두가지 설정을 반영하면, 일단 PathVariable을 입력으로 받는데 문제는 없다.

RESTful Endpoint Request

받는걸 살펴봤으니, 이제 보내는 걸 살펴보도록 하자.  PathVariable로 값을 전달하는 경우, 마찬가지로 / 가 들어가면 여러 가지가지 문제를 일으킨다.  가장 간단한 방법은 /가 포함된 값을 URLEncode로 encoding해버리면 될거다… 라고 생각할 수 있다.  하지만 Spring에서 우리가 흔히 사용하는 RestTemplate을 끼고 생각해보면 예상외의 문제점에 봉착한다.

간단히 고생한 걸 정리해보자면…

Get 요청을 하면 되는 것이기 때문에 RestTemplate에서 제공하는 endpoint.get(…)을 사용해 처음 작성을 했었다. 간단한 테스트 케이스에 대해서는 잘 작동을 했지만, 같이 개발하는 친구의 playerId값에는 / 가 포함되어 500 오류를 발생시켰다.

    @Autowire
    RestTemplate endpoint;
    ....

    public SomeResponse queryList(String playerId) {
        ResponseEntity<Log[]> response;
        response = endpoint.getForEntity("http://localhost:8080/api/v1/log/" + playerId, Log[].class);

        Log[] logs = response.getBody();
        ....
    }

위의 코드가 문제 코드인데 보면 아무 생각없이 playerId라는 값을 GET Operation의 path variable의 값으로 넣었다. / 가 없는 경우에는 별 문제가 없지만, 이게 있는 경우에는 “http://localhost:8080/api/v1/log/뭐시기뭐시기/지랄맞을”와 같은 형식이 되버린다. 받는 쪽에서 이걸 제대로 인식할리가 없다.
앞에서 이야기한 것처럼 URLEncoder.encode()를 걸어봤지만, %2F 값을 RestTemplate내에서 %252F로 encoding을 한번 더 해버리는 경우가 발생한다. 뭐 받는쪽에서 한번 더 decoding을 하면 되는거 아냐?? 라고 이야기할 수도 있겠지만, 그건 제대로 된 방법이 아니다.

이래 저래 해결 방법을 찾아봤는데 단순 getForObject, getForEntity의 소스 코드 내용을 확인해봤을 때는 제대로 사용이 어렵고, URITemplate을 사용하는 메소드를 가지고 처리를 해주는게 정답이었다. 다행히 exchange 계열 메소드에서 이걸 지원한다.

public <T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType, Object... uriVariables) throws RestClientException {
        Type type = responseType.getType();
        RequestCallback requestCallback = this.httpEntityCallback(requestEntity, type);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = this.responseEntityExtractor(type);
        return (ResponseEntity)this.execute(url, method, requestCallback, responseExtractor, uriVariables);
    }
...
    public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback, ResponseExtractor<T> responseExtractor, Object... urlVariables) throws RestClientException {
        URI expanded = this.getUriTemplateHandler().expand(url, urlVariables);
        return this.doExecute(expanded, method, requestCallback, responseExtractor);
    }
...

RestTemplate의 execute 메소드의 소스 코드에서 포인트는 UriTemplateHandler를 통해 변수를 바인딩한다는 것이다.

        response = endpoint.exchange("http://localhost:8080/api/v1/log/{playerId}", HttpMethod.GET, HttpEntity.EMPTY, Log[].class, playerId);

exchange 메소드는 전달된 parameter를 내부적으로 variable mapping을 한다는 점이다. 그리고 / 를 encoding하게 만들려면 추가적으로 RestTemplate 객체를 생성할 때 강제로 / 를 처리하라고 지정을 해줘야한다. RestTemplate 객체를 생성하는 @Bean 메소드쪽에서 아래와 같이 defaultUrlTemplateHandler 객체를 생성한다.

        RestTemplate template = new RestTemplate(factory);

        DefaultUriTemplateHandler defaultUriTemplateHandler = new DefaultUriTemplateHandler();
        defaultUriTemplateHandler.setParsePath(true);
        template.setUriTemplateHandler(defaultUriTemplateHandler);

setParsePath() 메소드의 값을 true로 설정한 default handler를 RestTemplate의 기본 핸들러로 설정해준다. 기본 설정이 false이기 때문에 / 가 Path variable에 들어가 있다고 하더라도 따로 encoding처리가 되지 않아 문제가 발생했다.

이렇게 설정 및 실행 방법 등등을 변경하고 실행하면… 문제 해결.

ㅇㅋ

이렇게 마무리하면 된다.

]]> 338
NamedApiEndpoint: 마이크로서비스를 더욱 더 마이크로하게! /index.php/2017/03/19/discoverable-api-endpoint-for-microservice/ Sun, 19 Mar 2017 05:16:04 +0000 /?p=320

Continue reading ‘NamedApiEndpoint: 마이크로서비스를 더욱 더 마이크로하게!’ »]]> 마이크로서비스 아키텍처가 개발자에게 주는 가장 좋은 점 가운데 하나는 배포의 자유로움이다.

일반적으로 마이크로서비스를 지향하는 서비스 시스템은 Monolithic 서비스과 대조적으로 제공하는 기능의 개수가 아주 작다.  따라서 고치는 것이 그만큼 훨씬 더 자유롭다.  Jenkins의 Build now 버튼을 누르는데 주저함이 없다고나 할까…

하지만 얼마나 잘게 쪼갤 것인가? 큰 고민거리다. QCon 컨퍼런스에서도 이야기가 있었지만, 최선의 방식은 가능한 작게 쪼개는 것이다.  서비스 시스템이 정의되면 이를 위한 http(s):// 로 시작하는 엔드포인트가 만들어진다. 그리고 다른 서비스 시스템들이 이를 코드(정확하게 이야기하자면 Configuration이겠지만)에 반영해서 사용한다.  이 과정에서 문제가 발생할 가능성이 생긴다.

  • 엔드포인트는 고정된 값이다.  보통 엔드포인트를 VIP 혹은 ELB를 가지고 사용하면 변경될 가능성이 적긴하다.  그렇지만 변경되면(!!) 사건이 된다.  변경에 영향을 받는 시스템들을 변경된 값에 맞춰 작업해줘야 한다. 이렇게 보면 이게 Monolithic 시스템과 뭔 차이인지 하는 의구심마저 든다.
  • 초보자는 이름을 헤메게 된다.  잘게 쪼개진 그 이름들을 다 알 수 있을까?  이런건 어딘가에 정말 잘 정리되어 있어야 찾을까 말까다.  연동할 시스템을 찾아 삼만리를 하다보면 짜증도 나고, 이게 뭐하는 짓인지 의구심마저 들게 된다.  특히나 좀 후진(개발 혹은 QA) 시스템들은 DNS를 등록해서 사용하지도 않기 때문에 더욱 난맥상을 빠질 수 있다.

이런 고민을 놓과 봤을 때 가장 합리적인 결론은 “정리된 목록“이다. 언제나 승리자는 정리를 잘 하는 사람이다. 하지만 시스템이 사람도 아니고… 목록이 있다고 읽을 수 있는건 아니지 않은가???

읽을 수 있다. 물론 그 목록이 기계가 읽을 수 있는 포맷이라면!!! 읽을 수 있다는 사실은 중요하다.  이제 사람이 알아들을 수 있는 “말”을 가지고 기계(시스템)를 다룰 수 있을 것 같다.

http://pds26.egloos.com/pds/201404/12/99/c0109099_5348f03fbc7cd.jpg

가능할 것 같으니 해봐야지!  NamedApiEndpoint라는 Github 프로젝트로 해봤는데, 나름 잘 동작하는 것 같다.

  • 동작하는 건 앞서 이야기한 것처럼 서비스 시스템의 엔드포인트를 이름으로 참조한다.
  • 이름 참조를 위해서는 { name, endpoint } 쌍을 보관하는 별도의 Repository가 물론 있어야 한다.
  • NamedApiEndpoint에서는 스프링 프레임워크(Spring framework)를 바탕한다.
  • 어플리케이션 시작 시점에 이름을 기초로 엔드포인트를 쿼리한다.
  • 쿼리된 엔드포인트 정보와 URI 정보를 이용해 Full URL을 구성해서 RestTemplate와 동등한 Operation set을 제공한다.

Repository는 별도 시스템으로 만들수도 있지만, Serverless 환경으로 이를 구성시킬 수도 있다.  내부적으로는 AWS DynamoDB를 API G/W를 연동해서 Repository 서비스로 만들었다.  이 부분에 대한 내용은 다른 포스팅에서 좀 더 다루겠다.  분명한 사실은 재미있다라는 거!

빙빙 돌려 이야기를 했지만 Service discoverous라는 개념이다.  관련해서 해본다.  기본적으로 Discovery와 Registry의 개념을 어떤 방식으로 구현하는지에 따라 달리겠지만 Spring 혹은 Springboot를 주로 많이 사용하는 환경에서 사용하기에 큰 문제는 없는 것 같다.  굳이 Well-known 방식을 선호한다면 링크한 페이지를 잘 체크해보면 되겠다.

– 끝 –

]]> 320
SpringBoot 1.4 기반의 Integration Test 작성하기 /index.php/2017/01/04/integration-test-in-springboot-1-4/ Tue, 03 Jan 2017 16:07:07 +0000 /?p=292

Continue reading ‘SpringBoot 1.4 기반의 Integration Test 작성하기’ »]]> 기본적인 내용은 을 바탕으로 한다.   한글로 읽기 귀찮다면 링크된 본문을 참고하자!!

 

스프링 부트는 복잡한 설정없이 손쉽게 웹 어플리케이션 서버를 실행할 수 있다는 점때문에 자바 언어 세계에서 널리 사용되어오고 있다.  부트 역시 스프링 프레임워크에서 지원했던 방식과 유사한 형식의 테스트 방식을 지원하고 있었다.  하지만 부트의 테스트는 부트 자체가 웹 어플리케이션 개발을 쉽게 했던 만큼은 더 나아가지 못했다.

가장 간단한 테스트를 작성하는 방법은 물론 스프링을 배제한 형태다.  스프링의 각종 @(어노테이션 – Annotation)을 벗어난 코드라면 이 방식의 테스트가 가장 적절하다.  사람들이 오해하는 것 가운데 하나는 @Component 혹은 @Service라고 어노테이션이 붙은 클래스를 테스트할 때 꼭 스프링을 끼고 해야한다고 생각한다는 점이다.

굳이 그럴 필요가 없다.

  • 객체는 그냥 new 를 이용해서 만들면 된다.
  • @Value 어노테이트된 값은 그냥 값을 셋팅하면 된다.
  • 테스트 대상 메소드는 대부분 public 키워드를 갖는다. 테스트 메소드에서 접근을 걱정할 일이 거의 없다.
  • 외부에 공개되지 않은 메소드를 테스트해야하거나 아니면 @Value 값을 강제로 설정해야하는 경우라면 아예 테스트 대상 클래스를 상속받는 클래스를 클래스를 테스트 패키지에 하나 만든다.  그럼 원형을 해칠 필요도 없고, 딱 테스트에 적합한 형태로 맘대로 가지고 놀 수 있다.

 

하지만 스프링과 엮인 부분들이 많다면 테스트에 스프링을 끼지 않을 수 없다. 난감하다. 스프링과 함께 테스트를 돌릴 때 가장 난감한 점은 테스트 실행 시간이 꽤 든다는 점이다.  특히 매 테스트마다 어플리케이션 전체가 올라갔다가 내려갔다를 반복된다.  단위 테스트라는 말을 쓸 수 있을까?  그래서 그런지 통합 테스트(Integration Test)라고 이야기하는 경우가 많다.  이런 것이 싫었던 것도 있어서 정말 Integration Test가 아니면 웬만하니 테스트를 잘 작성하지 않았다.

이러던 것이 스프링부트 1.4 버전을 기점으로 좀 더 쉬운 형태로 테스트 작성 방법이 바뀐 사실을 알게 됐다.  예전게 못먹을 거라고 생각이 들었던 반면에 이제는 좀 씹을만한 음식이 된 것 같다.  사실 부트 버전을 1.4로 올린 다음에 테스트를 돌려볼려고 하니 Deprecated warning이 이전 테스트에서 뜨길래 알았다.  그냥 쌩까고 해도 별 문제가 없을 것을 왜 이걸 굳이 봤을까…

이미 봐버렸으니 역시나 제대로 쓸 수 있도록 해야하지 않을까?  이제부터 언급하는 스프링 테스트는 부트 1.4 바탕의 테스트 이야기다.

@SpringBootApplication
public class Application {
   public static void main(String[] args) throws Exception {
      SpringApplication.run(IPDetectionApplication.class, args);
   }
}

테스트를 작성할려면 가장 먼저 어플리케이션 자체의 선언이 @SpringBootApplication 어노테이션으로 정의되야만 한다.  부트 어플리케이션이 되는 방법은 이 방법말고 여러 방법이 있지만 1.4의 테스트는 꼭 이걸 요구한다. 안달아주면 RuntimeException을 낸다.  강압적이지만 어쩔 수 없다.

환경을 갖췄으니 이제 테스트를 이야기하자.  가장 간단히 테스트를 돌리는 방법은 테스트 클래스에 다음 두개의 어노테이션을 붙혀주면 된다.

@RunWith(SpringRunner.class)

@SpringBootTest

이 두 줄이면 스프링 관련 속성이 있는 어떤 클래스라도 아래 코드처럼 테스트할 수 있다.

@RunWith(SpringRunner.class)
@SpringBootTest
@Log4j
public class SummonerCoreApiTest {
    @Autowired
    private SummonerCoreApi summonerApi;

    @Test
    public void shouldSummonerApiLookUpAnAccountWithGivenAccountId() {

다 좋은데 이 방식의 문제점은 실제 스프링 어플리케이션이 실행된다는 점이다.  물론 테스트 메소드(should…)가 끝나면 어플리케이션도 종료된다.  @SpringBootTest 라는 어노테이션이 주는 마력(!!)이다.  만약 Stage 빌드를 따르고 있다면 테스트를 위한 환경을 별도로 가질 수 있다.  메이븐을 이용해서 환경을 구분하는 경우에는 별 문제가 없지만 만약 스프링 프로파일(Spring profile)을 이용하고 있다면 별도로 지정을 해줘야 한다.  @ActiveProfiles(“local“) 어노테이션을 활용하면 쉽게 해당 환경을 지정할 수 있으니 쫄지 말자.

OMG

어떤 클래스라도 다 테스트를 할 수 있다!!  몇 번에 걸쳐 이야기하지만 어플리케이션 실행에 관련된 모든게 다 올라와야하기 때문에 시간이 오래 걸린다.  좀 더 현실적인 타협안이 있을까?

일반적으로 Integration Test는 RESTful 기반 어플리케이션의 특정 URL을 테스트한다.  따라서 테스트 대상이 아닌 다른 요소들이 메모리에 올라와서 실행 시간을 굳이 느리게 할 필요는 없다.  타협안으로 제공되는 기능이 @WebMvcTest 어노테이션이다.  어노테이션의 인자로 테스트 대상 Controller/Service/Component 클래스를 명시한다.  그럼 해당 클래스로부터 참조되는 클래스들 및 스프링의 의존성 부분들이 반영되어 아래 테스트처럼 실행된다.

@RunWith(SpringRunner.class)
@WebMvcTest(AccountInfoController.class)
@Log4j
public class AccountInfoControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    public void shouldControllerRespondsJsonForTheGivenUsername() throws Exception {
        final String givenUsername = "chidoo";

        GIVEN: {
        }

        final ResultActions actualResult;
        WHEN: {
            actualResult = mvc.perform(get("/api/v1/account/name/" + givenUsername)
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8));
        }

        THEN: {
            assertThat(actualResult.getPlayerId(), is(...));
        }
    }
    ...

이 테스트는 Integration Test이다.  역시나 Integration Test는 비용이 많이 들고, 테스트 자체가 제대로 돌도록 만드는 것 자체가 힘들다.  DB를 연동하면 DB에 대한 값도 설정을 맞춰야하고, 외부 시스템이랑 연동을 한다면 것도 또 챙겨야한다.

포기할 수 없다. 이걸 단위 테스트할려면 어떻게 해야하는거지?  Mocking을 이용하면 된다!!!  테스트 대상 코드에서 “스프링 관련성이 있다.“는 것의 대표는 바로 @Autowire에 의해 클래스 내부에 Injection되는 요소들이다.   이런 요소들이 DB가 되고, 위부 시스템이 된다.  해당 부분을 아래 코드처럼 Mocking 방법을 알아보자.

  • 테스트 대상 코드의 내부 Injecting 요소를 @MockBean이라는 요소로 선언한다.  이러면 대상에서 Autowired 될 객체들이 Mocking 객체로 만들어져 테스트 대상 클래스에 반영된다.
  • 이 요소들에 대한 호출 부위를 BDDMockito 클래스에서 제공하는 정적 메소드를 활용해서 Mocking을 해준다.
  • given/willReturn/willThrow 등 과 같이 일반적인 Mockito 수준에서 활용했던 코드들을 모두 활용할 수 있다.

그럼 명확하게 클래스의 동작 상황을 원하는 수준까지 시뮬레이션할 수 있다.

@RunWith(SpringRunner.class)
@WebMvcTest(AccountInfoController.class)
@Log4j
public class AccountInfoControllerTest {
    private JacksonTester<AccountInfo> responseJson;
    private JacksonTester<AccountInfo[]> responseJsonByPlayerId;

    @Autowired
    private MockMvc mvc;

    @MockBean
    private AccountInfoService accountInfoService;

    @Before
    public void setup() {
        JacksonTester.initFields(this, new ObjectMapper());
    }

    @Test
    public void shouldControllerRespondsJsonForTheGivenUsername() throws Exception {
        final String givenUsername = "chidoo";
        final AccountInfo expectedAccountInfo;

        GIVEN: {
            expectedAccountInfo = new AccountInfo("dontCareAccountId", givenUsername, "dontCarePlayerId");
            given(accountInfoService.queryByUsername(givenUsername)).willReturn(expectedAccountInfo);
        }

        final ResultActions actualResult;
        WHEN: {
            actualResult = mvc.perform(get("/api/v1/account/name/" + givenUsername)
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8));
        }

        THEN: {
            actualResult.andExpect(status().isOk())
                    .andExpect(content().string(responseJson.write(expectedAccountInfo).getJson()));
        }
    }
    ...

특정 서비스를 테스트하는데 전체를 다 로딩하지 않고, 관련된 부분 모듈들만 테스트 하고 싶은 경우에는 아래 코드를 참고한다.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { SomeService.class, InterfacingApi.class, CoreApi.class})
@Log4j
public class SomeServiceTest {
    @Autowired
    private SomeService service;

   @Test
    public void shouldServiceQueryAccount() {
        final long accountIdAsRiotPlatform = 1000l;
        GIVEN: {}

        final Account account;
        WHEN: {
            account = service.lookup(accountIdAsRiotPlatform);
        }

        THEN: {
            assertThat(account.getAccountId(), is(accountIdAsRiotPlatform));
        }
    }
}

만약 Controller를 메소드를 직접 호출하는게 아니라 MockMvc를 사용해 호출하는 경우라면 다음 2개의 어노테이션을 추가하면 된다. 이러면 테스트할 대상 하위 클래스를 지정할 수 있을뿐만 아니라 MockMvc를 활용해서 Controller를 통해 값을 제대로 받아올 수 있는지 여부도 명시적으로 확인할 수 있다.

@AutoConfigureMockMvc
@AutoConfigureWebMvc

Swagger 관련 오류에 대응하는 방법

JPA 관련된 테스트 혹은 WebMvcTest를 작성하는 과정에서 아래와 같은 Exception을 만나는 경우가 있다.

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.util.List<org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1466)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1097)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1059)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
	... 58 more

별거 한거 없이 정석대로 진행을 했는데 이런 뜬금없는 오류가 발생하는 상황에서는 프로젝트에 Swagger 관련된 설정이 있는지 확인해봐야한다. Swagger에 대한 Configuration에서 다른 설정을 해주지 않았다면 스프링부트 테스트는 Swagger와 관련된 Bean 객체들도 실행시킬려고 한다. 이걸 회피하기 위해서는 다음의 두가지 설정을 Swagger쪽과 문제가 되는 테스트쪽에 설정해줘야 한다.

Swagger 설정

@Configuration
@EnableSwagger2
@Profile({"default", "local", "dev", "qa", "prod"})
public class SwaggerConfiguration {
...
}

스프링부트가 기본으로 스프링 프로파일을 사용한다. 본인이 스프링 프로파일을 사용하든 안하든… 위의 케이스처럼 스프링이 적용될 환경을 명시적으로 정의해둔다. 대부분의 환경이 위의 5가지 환경으로 분류되고, 별도로 명시하지 않으면 default가 기본 프로파일로 잡힌다. 물론 테스트 케이스를 실행하는 경우에도 명시적으로 해주지 않으면 default 프로파일이 적용된다. 때문에 이제 테스트가 이를 회피하기 위한 프로파일을 명시해준다.

테스트 클래스 설정

@RunWith(SpringRunner.class)
@DataJpaTest
@ActiveProfiles("test")
@Log4j
public class AccountInfoServiceTest {
...
}

이렇게 실행되면 테스트가 실행될 때 test라는 프로파일을 가지기 때문에 Swagger 관련 모듈들을 로딩하지 않고, 테스트가 실행된다.

일단 여기까지 정리해봤다.

]]> 292
Springboot에서 Exception을 활용한 오류 처리 /index.php/2016/11/02/springboot-exception-handling/ Wed, 02 Nov 2016 06:44:03 +0000 /?p=254

Continue reading ‘Springboot에서 Exception을 활용한 오류 처리’ »]]> Java를 가지고 개발하는 오류 처리는 Exception을 활용하는 것이 정석이다.  개인적으로 값을 오류 체크하고 어떻게든 값을 만들어 반환하기보다는 오류가 발생하면 “오류다!” 라고 떳떳하게 선언하는 것이 좋은 방법이라고 생각한다.

RESTful API를 구현한 경우,  오류의 상태를 알려주는 가장 정석적인 방법은 HTTP Status Code를 활용하는 방법이다.  Exception을 통해서 이 응답 코드를 반환해주는 건 아주 쉽다.

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason="No candidate")
public class NotExistingCandidateException extends RuntimeException {
    public NotExistingCandidateException(String candidateEmail) {
        super(candidateEmail);
    }
}

이것과 관련해서 약간 말을 보태본다.  RESTful API를 개발하면서 응답 메시지의 Body에 상태 코드와 응답 메시지를 정의하는 경우를 왕왕본다.  RESTful 세상에서 이런 방식은 정말 안좋은 습관이다.   몇 가지 이유를 적어보면.

  1. 완전 서버 혹은 API를 만든 사람 중심적이다.  클라이언트에서 오류를 알기 위해서는 반드시 메시지를 까야한다.  호출한 쪽에서는 호출이 성공한 경우에만 처리하면 되는데 구태여 메시지를 까서 성공했는지 여부를 확인해야한다.
  2. API 클라이언트의 코드를 짜증나게 만들뿐만 아니라 일관성도 없다.  한 시스템을 만드는데 이런 자기중심적인 사람이 서넛되고, 상태 변수의 이름을 제각각 정의한다면 어떻게 되겠나?  헬이다.
  3. 표준을 따른다면 클라이언트 코드가 직관적이된다. Ajax 응답을 처리한다고 했을 때 성공은 success 루틴에서 구현하면 되고, 오류 처리는 error 구문에서 처리하면 된다.  프레임웍에서 지원해주기 때문에 코드를 작성하는 혹은 읽는 사람의 입장에서도 직관적이다.  더불어 성공/실패에 대한 또 다른 분기를 만들 필요도 없다.
  4. 이 경우이긴 하지만 Exception을 적극적으로 활용하는 좋은 습관을 가지게 된다.  코드를 작성하면서 굳이 Exception을 아끼시는 분들이 많다. 하지만 Exception은 오류 상황을 가장 명시적으로 설명해주는 좋은 도구이다.  문제 상황에서 코드의 실행을 중단시키고, 명확한 오류 복구 처리를 수행할 수 있는 일관성을 제공하기 때문에 코드의 품질을 높일 수 있다.

표준이 있으니 표준을 따르자.  이게 사려깊은 개발자의 태도다.

ResponseStatus라는 어노테이션을 사용하면 Exception이 발생했을 때, 어노테이션에 정의된 API Response Status Code로 반환된다.  이때 주의할 점은 정의한 Exception이 반드시 RuntimeException을 상속받아야 한다는 것이다.  Throwable을 상속받거나 implement 하는 경우에는 어노테이션에 의해 처리되지 않기 때문에 주의하자.

    @ExceptionHandler(UnknownAccessCodeException.class)
    public String handleUnknownAccessCodeException() {
        return "unknown";
    }

만약 Thymeleaf와 같은 UI framework을 사용해서 특정 오류 케이스가 발생했을 때 특정 뷰를 제공하고 싶다면, Controller 수준에서 ExceptionHandler 어노테이션을 활용할 수 있다.  위 예제는 Exception이 발생했을 때 unknown이라는 Thymeleaf의 뷰를 제공하라고 이야기한다.  이때도 주의할 점은 Exception은 반드시 RuntimeException을 상속해야한다는 점이다.

만약 어플리케이션 전반적으로 Exception에 대한 처리 로직을 부여하고자 한다면 ControllerAdvice 어노테이션을 활용한다.  어노테이션을 특정 클래스에 부여하면 해당 클래스에서 정의된 Exception 핸들러들이 전역으로 적용된다.

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // Nothing to do
    }
}

상세한 정보는 에서 참고하면 된다.

]]> 254
Spring Data JPA를 활용한 DAO를 바꿔보자. /index.php/2016/05/08/spring-data-jpa-for-short-memories/ Sat, 07 May 2016 16:21:37 +0000 /?p=130

Continue reading ‘Spring Data JPA를 활용한 DAO를 바꿔보자.’ »]]> 부트 이전에 스프링에서 데이터베이스를 그래도 다른 사람이 쓰는 만큼 쓸려면 MyBatis를 써줘야했다.

MyBatis를 한번이라도 써본 사람이라면 알겠지만 복잡하다. 스프링 XML 설정의 복잡도에 MyBatis의 복잡도를 더하면 상당히 헬 수준으로 올라간다. 단순 목록 하나만 가져오는데 MyBatis를 쓰는건 형식주의에 빠진 불합리의 최상급이었다. 되려 JDBC를 가져다가 prepareStatement에 Bind 변수만 사용하는 것이 오히려 손쉽게 직관적일 수 있다. 글을 읽는 분들중에 Bind 변수를 쓴다는 말을 이해하지 못하시는 분들은 이해의 수고를 덜기 위해 그냥 MyBatis를 쓰는게 정신 건강에 좋다. 하지만 적극 추천한다.

부트(Springboot)를 공부하면서 대부분 Annotation을 가지고 처리하는데 데이터베이스에 대한 접근도 비슷한 방법이 없을까 싶어서 잠깐 찾아봤었지만 역시… JPA 라는 걸 이미 많이 사용하고 있었다.  그리고 MyBatis처럼 설정 그까이게 거의 없다.  테이블 혹은 쿼리와 맵핑되는 설정 몇 가지만으로도 바로 이 기능으로 데이터베이스에서 자료를 읽어내거나 저장할 수 있다.

개인적으로 데이터베이스를 엑세스하는 쿼리를 복잡하게 가져가는건 별로 좋은 방법이 아니라고 굳게 믿는다. 폄하하자면 프로그래밍을 못짜는 개발자들이 논리의 부재를 쿼리로 입막음하려는 경향이 있다.  정말 잘못된 자세다. 정신 머리를 고쳐먹고 쿼리는 최대한 심플하게 작성하고 로직은 프로그램으로 대응하는 버릇을 들이도록 정신을 개조해라.

시작하기

개발을 할려면 먼저 이걸 사용하기 위한 준비부터 해야한다. 메이븐을 개발 환경으로 사용하기 때문에 아래 설정을 반영하면 된다. 데이터베이스 종류에 따라 해당 데이터베이스에 대한 추가적인 의존성을 가져가야한다는건 이미 알고 있으리라 생각한다. 그냥 귀찮으니까 MySQL에 대한 설정 부분만 Copy & Paste하기 좋게 추가해둔다.  그리고 앞으로 설명은 전부 MySQL을 기반으로 진행한다.

<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-jpa</artifactId>
  <version>1.10.1.RELEASE</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.6</version>
  <scrope>runtime</scope>
</dependency>

버전에 관련된 사항은 해당 를 통해 확인해보는게 좋다.

라이브러리는 준비됐으니까 이제 데이터베이스를 셋업해봐야겠다. 데이터베이스 셋업은 여기에서 설명하는 개발 주제랑은 무관하니까 일단 스킵. 하지만 개발자라면 꼭 자신의 로컬에 데이터베이스 하나쯤은 실행시켜서 쿼리 도구로 실행해봐야겠다.  그럼 이게 준비됐다라는 전제면 접속을 위한 설정 정보를 코드에 반영해둬야 한다. 이건 보통 application.properties 파일에 다음과 같이 잡아둔다.

spring.datasource.url=jdbc:mysql://localhost:3307/db_name
spring.datasource.username=admin
spring.datasource.password=********
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

응, 근데 JPA가 뭐지?

시작을 하긴 했지만 JPA가 뭐의 약자인지부터 알고 가자. JPA는 Java Persistence API의 약자. 2000년대 초반에 나왔다가 별로 빛을 보지 못했던 것 같다. 스프링 진영에서 아마 iBatis or MyBatis 류의 약진으로 뜰 기회가 아예 없었을 것 같다. 더구나 하나만 고집하는 한국 환경에서 이미 대세가 된 iBatis를 넘어서는 것이 불가능하다.

내가 이해하는 JPA의 컨셉은 데이터베이스에 존재하는 한 모델을 자바의 한 객체로 mapping하는데 목적이 있다.  이 방향성은 단방향성이 아니라 양방향성이다. 즉 코드상에 존재하는 객체는 마찬가지로 데이터베이스에서도 존재해야 한다.  이를 매개하는 존재가 ID 필드이다.  ID 필드는 특정 클래스/테이블의 Uniqueness를 보장하기 위해 사용된다.

jpa-concept

이 ID 필드는 클래스(객체)에 정의되지만 반대 급부로 테이블에도 마찬가지로 해당 필드가 정의되어야 한다. 다만 테이블과 다른 점은 객체를 통해 관리할 정보는 필요한 정보들에 국한될 수 있다.  즉 불필요한 정보는 굳이 객체로 관리할 필요가 없고, 따라서 클래스의 필드로 정의할 필요가 없다. 하지만 테이블 구조에서 Not null 필드 혹은 Constraint에 보호되는 필드라면 적절한 장치가 필요하긴 할 것이다.

단일 테이블부터

테이블 하나에 대한 처리는 정말 쉽다.  하지만 몇가지 지켜야 할 규칙이 있다.  우리가 다뤄야 할 테이블의 이름을 SAMPLE_TABLE이라고 가정하자.

create table SAMPLE_TABLE (
  id int not null autoincrement,
  sample_name varchar(100) not null,
  code varchar(20) not null,
  create_datetime datetime not null,
  primary key(id)
);
  • 테이블의 이름과 동일한 모델 클래스를 만든다. 따라서 클래스는 헝가리안 표기법(Hungarian Notation)을 따라 SampleTable 이라는 이름이어야 한다. 테이블 혹은 필드의 이름이 Underbar(‘_’)로 구분되면 각 단어의 처음이 대문자로 표현되어야 한다. 클래스의 이름을 통해 실제 테이블을 mapping하게 된다.
  • 테이블의 모든 컬럼에 대응하는 모든 필드를 만들 필요는 없으며 ID 필드를 포함한 다뤄야 할 필드들을 정의하면 된다. 쓸데없는 것까지 다룰 필요는 없다.
  • 테이블의 컬럼에 대응하는 필드의 이름은 카멜 표기법(Carmel Notation)을 따라 표기한다.
  • Serialize/Deserialize할 수 있어야 하기 때문에 테이블의 각 필드들에 대해 Getter/Setter를 만들어둬야 한다.  일일히 하기에 귀찮다. 를 사용하면 각 필드들에 @Getter @Setter Annotation을 사용하거나 전체 클래스에 대해 적용할려면 @Data Annotation을 적용하면 된다.
  • 스프링에서 해당 클래스를 참조할 수 있도록 @Entity라는 Annotation을 적용한다.
@Entity
@Data
public class SampleTable {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private long id;

    private String sampleName;
    private String code;
}

한 두개 팁을 확인해보자.

  • Entity 클래스와 테이블의 이름이 다른 경우 – Entity 클래스와 테이블의 이름이 틀려지면 @Table(name=”SAMPLE_TABLE”)을 지정한다.
  • 컬럼의 이름과 다른 필드의 이름을 부여하고 싶은 경우 – @Column(name = “COLUMN_NAME”) 어노테이션을 통해 해결한다.
  • 크기가 큰 컬럼들 – 테이블의 컬럼 타입이 CLOB 혹은 BLOB 같은 타입의 경우 자동으로 큰 크기를 처리할 수 없다.  이 경우에 mapping되는 컬럼이  해당 타입인지 여부를 @Clob 혹은 @Blob 같은 어노테이션을 통해 따로 지정해줘야 한다. (생각해보면 당연하다. CLOB/BLOB을 JDBC 혹은 Pro*C로 처리할 때 단순 Variable Binding을 가지고 처리할 수 없다는걸 아는 사람이라면. 그리고 왜 그래야만 하는지도.)

 

CRUD

데이터베이스에 대한 @Entity와 @Data 어노테이션을 활용해 데이터베이스 테이블과 클래스의 맵핑이 완료됐다. 그럼 실제로 동작이 되도록 이 두개를 Repository를 통해 이어주면 된다. 간단하다.

public interface YourSampleTableRepository extends CrudRepository<SampleTable, Long> {
    Layout findOne(Long id);
}

JPA에서 기본 제공해주는 CrudRepository 인터페이스를 상속해서 새로운 당신의 Repository를 만들면 된다. 그럼 기본은 전부할 수 있다. CrudRepository가 뭐하는건지 궁금하지 않은가? 이 인터페이스는 아래와 같이 생겨먹었다.

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
    <S extends T> S save(S var1);
    <S extends T> Iterable<S> save(Iterable<S> var1);

    T findOne(ID var1);
    Iterable<T> findAll();
    Iterable<T> findAll(Iterable<ID> var1);

    boolean exists(ID var1);
    long count();

    void delete(ID var1);
    void delete(T var1);
    void delete(Iterable<? extends T> var1);
    void deleteAll();
}

인터페이스의 생겨먹은 모습에서 알 수 있듯이 대부분의 동작들을 지원한다. (보기 편이를 위해서 약간 순서를 조정하긴 했다.) 그런데 흔하게 보다보면 CrudRepository라는 것 이외에 JpaRepository도 흔하게 사용한다. 아마도 처음 접하는 예제의 종류가 틀려서 그런가 싶기도 하지만… . 간단히 요약하자면

  • JpaRepository는 CrudRepository의 손자뻘 인터페이스이다.
  • JpaRepository는 Crud에 비해 게시판 만들기에 용이한 Paging개념과 배치 작업 모드를 지원한다.
  • 하지만 다 할 수 있다고 다 이걸로 쓰는건 아니다. 언제나 강조하지만 닭잡는데 소잡는 칼을 쓸 필요는 없지않은가?

다른 길로 빠지긴 했지만 일단 기본으로 쓸려면 그냥 CrudRepository를 기본 칼로 쓰는게 좋다. 상황에 따라 적절한 도구를 사용하고 있는가 혹은 사용할 수 있는 도구를 고를 수 있는 자질이 되는가를 스스로 질문해보자. 답변을 할 수 있는 역량이 본인에게 있다면 다행이다.

Queries

만들고, 변경하고, 지우고 있는지를 확인하고 등등 기본적인 동작이 되는건 대강 확인했다. 그럼 특정 상황에 맞는 조회 문장을 만드는 건 Repository 인터페이스에 조회용 메소드를 정의함으로써 이뤄진다. JPA 환경에서 특정 조건을 만족하는 쿼리를 수행하는 방법은 크게 아래와 같은 가지수로 나뉜다. (상세한 설정에 대한 도움말은 역시 .)

  • Query creation from method – 쿼리를 유추할 수 있는 메소드의 이름을 통해 쿼리를 정의한다.  일반적인 규칙은 findBy뒤에 조건이 되는 필드의 이름을 And 혹은 Or 조건을 섞어 작성한다. 실행에 필요한 값들은 언급된 매개 변수의 순서에 맞춰서 작성한다.
public interface UserRepository extends Repository<User, Long> {

  List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}

이 쿼리가 대강 실행되면 대강 아래와 같은 쿼리의 형식으로 실행된다.

select u from User u where u.emailAddress = ?1 and u.lastname = ?2
  • NamedQuery annotation – 설정을 xml 파일에 두는 경우, 해당 파일의 이름은 orm.xml이어야 한다. 그게 아니면 테이블에 맵핑된 클래스(이걸 Domain Class라고 부르는군.)에 @NamedQuery라는 annotation을 사용해서 실행될 쿼리를 아래 예제처럼 적어주면 된다.
<named-query name="User.findByLastname">
  <query>select u from User u where u.lastname = ?1</query>
</named-query>
@Entity
@NamedQuery(name = "User.findByEmailAddress", query = "select u from User u where u.emailAddress = ?1")
public class User {
    ...
}

public interface UserRepository extends Repository<User, Long> {
  List<User> findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}
  • Query annotation – 가장 흔하게 사용하는 방법이다. Repository의 조회 메소드에 직접 실행될 쿼리를 적어준다.
public interface UserRepository extends JpaRepository<User, Long> {
  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}

JPA는 조건문에 들어가는 파라미터의 위치를 메소드의 파라미터 위치로부터 유추한다. 즉 ?1 이라는 표시된 곳에 첫번째 파라미터의 값이 반영된다. 쿼리가 복잡하다면 이름을 참조하는 좀 더 알아보기 쉬운 방식으로 사용하는게 좋다. 이름을 참조할려면 인터페이스 메소드의 각 파라미터 앞에 @Param 이라는 추가 annotation을 사용해서 참조 가능한 이름을 주고 쿼리에서 이 이름을 사용하면 된다.

public interface UserRepository extends JpaRepository<User, Long> {
  @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
  User findByLastnameOrFirstname(@Param("lastname") String lastname, @Param("firstname") String firstname);
}

예제에서 보면 쿼리에 firstname, lastname이라는 변수를 참조했고, 참조된 변수는 @Param annotation으로 메소드에 함께 선언된 것을 확인할 수 있다.  복잡하다면 이렇게 해주는게 읽는 사람을 위한 배려다.

그래도 테이블 두개는 조인할 수 있어야지

테이블 한개에 대한 처리는 지금까지의 설명으로 충분하다. 하지만 2개 이상의 테이블을 조인해서 뭔가를 추출하는 경우라면 어떻게 해야할까?  쿼리상으로는 간단히 조인을 걸어서 결과를 확인하는 아주 간단한 거다. JPA 방식에서 사용할려면 좀 더 터치가 필요하다.  간단히 다음과 같은 테이블 구조가 있다고 하자.

joined

여기에서 사용자쪽의 정보를 바탕으로 그룹에 대한 정보를 가져올려고 한다면


select user.userid, password, group.groupid, ... from user join group on user.group_id = group.group_id

와 같이 하면 된다.  이걸 JPA 기반에서 처리한다면


@Entity
@Data
public class User {
    ...
    @ManyToOne
    @JoinTable(name="USER_GROUP")
    Group group;
};

@Entity
@Data
public class Group {
};

흠.. 이렇게 하면 될까? 실제로 해보질 않아서 모르겠다.

JPQL이라는 걸 JPA 내부적으로 사용한다고 한다. 하지만 파보질 않아서 잘 모르겠네.

상세한 내용은 다음 링크에서 도움을 나중에 받을 수 있을 것 같다.

  • https://en.wikibooks.org/wiki/Java_Persistence/Querying
  • http://www.objectdb.com/java/jpa/query/jpql/from

일단 미완.

Tips

MySQL의 경우에 테이블 이름을 Underscore(_)로 구분하는게 아니라 대문자 형식(ex: MyTable)으로 작성하는 경우가 있다. 이 경우에는 Entity의 테이블 이름을 MyTable로 주면 Spring JPA에서 자동으로 my_table로 변경해버려서 테이블을 찾지 못한다고 이야기하는 경우가 있다.  이때는 해주면 된다.

spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

한참을 헤메긴 했지만 알고 있다면 좋을 것 같다.

]]> 130
Maven을 이용해서 신규 프로젝트 만들기 /index.php/2016/04/26/creating-maven-project-by-command/ Tue, 26 Apr 2016 04:23:58 +0000 /?p=107

Continue reading ‘Maven을 이용해서 신규 프로젝트 만들기’ »]]> 한땀한땀 손으로 Maven 프로젝트를 만드는 것도 의미있는 일이지만 귀찮다.

mvn archetype:generate -DinteractiveMode=false -DarchetypeArtifactId=maven-archetype-quickstart \
-DgroupId={project-packaging} -DartifactId={project-name}

와 같은 형태로 잡아주면 된다.
최근 개발은 Spring Boot를 많이 이용하기 때문에 여기에서 주로 쓸만한 archetype들을 나열해보면

  • maven-archetype-quickstart
  • spring-boot-sample-simple-archetype
  • spring-boot-sample-data-jpa-archetype
  • spring-boot-sample-actuator-log4j-archetype

Spring에서 사용할 수 있는 전체 하다.  다만 Spring 기반으로 프로젝트를 생성시킬려면 기본 archetypeArtifactId 이외에 archetypeGroupId=org.springframework.boot 값을 추가로 줘야한다.

mvn archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=org.springframework.boot -DarchetypeArtifactId={spring-archetype} \
-DgroupId={project-packaging} -DartifactId={project-name}

가장 대표적인 API 개발용 명령을 이용하는게 가장 깔끔하다.

mvn archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=org.springframework.boot -DarchetypeArtifactId=spring-boot-sample-simple-archetype \
-DgroupId={project-packaging} -DartifactId={project-name}

그 다음에 생성된 pom.xml 파일의 spring-boot-starter-parent 의 버전을 1.3.3.RELEASE로 변경한다. CORS 지원과 몇가지 기능을 사용할려면 이 이상 버전을 사용하는게 좋다. 추가적으로 다음의 Dependency들을 pom.xml에 반영하면 즐거운 코딩 생활에 도움을 얻을 수 있다.

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.16.8</version>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.4</version>
    </dependency>
    <!-- for db programming with mysql -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
      <scope>runtime</scope>
    </dependency>

한가지를 더 추가하자면 람다등을 사용할려면 자바 컴파일 환경을 1.8 이상으로 설정하는게 편하다. 다음 빌드 플러그인을 설정해두면 대부분의 IDE에서 인식한다.

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>utf-8</encoding>
        </configuration>
      </plugin>

즐 코딩~

 

]]> 107