'전체'에 해당되는 글 376건

  1. 반찬 사이트 비교 추천
  2. Spring Boot, REST API 예외 응답 로직 작성하기
  3. Spring Boot, ExceptionHandler, 전역 예외 처리 로직 작성하기
  4. Spring, MyBatis, Domain, DAO, Mapper XML 작성하기
  5. PES 2017, 마스터리그 팀 추천, 레알 마드리드
  6. Java, UUID 생성하기
  7. Spring Boot, MyBatis, MySQL, MariaDB 레플리케이션 구현하기
  8. Spring Boot, JVM 옵션 설정하기
  9. 드미트리렌더(DmitriRender) 설치 및 구매하기
  10. REST API와 국제화 (Internationalization, i18n)

반찬 사이트 비교 추천

더반찬

  • 더반찬은 가장 유명하고 점유율이 높은 반찬 사이트이다. 2016년 동원홈푸드가 인수하여 운영 중이다.
  • 기본 배송비는 2,500원이다. 배송비 무료 조건은 45,000원 이상 주문할 경우이다.
  • 매주 목요일 21:00마다 메뉴가 갱신되는 7데이세트가 유명하다. 주어진 반찬, 국 메뉴 중에 자유롭게 개수를 선택하여 주문하는 방식이다.

배민프레시

  • 배민프레시더반찬에 이어 점유율 2위 반찬 사이트이다.
  • 집밥의 완성 맛보기세트는 반찬 5가지에 9,900원에 판매된다.

비움반찬

  • 비움반찬은 맛의 고장 전남 광주를 기반으로 하는 반찬 사이트이다.
  • 당일 13시 주문 건만 당일 조리 후 출고되며 광주 외 지역은 익일 배송된다.
  • 기본 배송비는 4,000원이다. 배송비 무료 조건은 25,000원 이상 주문할 경우이다.
  • 할인메뉴 중 판타스틱9 세트메뉴가 34,000원에 판매된다.
저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, REST API 예외 응답 로직 작성하기

목표

  • 아래와 같이 REST API 오류 응답시 클라이언트가 이해할 수 있는 충분한 정보를 제공한다.
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
   "error_code":"INVALID_CLIENT_ID",
   "error_message":"The requested client identifier is invalid.",
   "error_data":{
      "client_id":"x9LHxnqkFp9vcEfUlsCtBG"
   }
}

ExceptionData 설계

package com.jsonobject.example.api.domain;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.HashMap;
import java.util.Map;

@NoArgsConstructor
@Data
public class ApiExceptionData {

    private Map<String, Object> data;

    public ApiExceptionData add(String key, Object value) {

        if (this.data == null) {
            this.data = new HashMap<String, Object>();
        }
        this.data.put(key, value);

        return this;
    }
}

Exception 설계

package com.jsonobject.example.api.exception;

import com.jsonobject.example.api.domain.ApiExceptionData;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;

@NoArgsConstructor
@Getter
@Setter
@ToString
@Slf4j
@JsonPropertyOrder(value = {"error_code", "error_message", "error_data"})
public class ApiException extends Exception {

    @JsonProperty("error_code")
    private String code;

    @JsonProperty("error_message")
    private String message;

    @JsonProperty("error_data")
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Map data;

    public ApiException(String code, String message) {

        this.code = code;
        this.message = message;
    }

    public ApiException(String code, String message, ApiExceptionData data) {

        this.code = code;
        this.message = message;
        this.data = data.getData();
    }
}

ExceptionHandler 설계

이제 마지막으로 ExceptionHandler를 작성하여 설계한 예외 발생시 후처리 로직을 설계해야 한다. 본 블로그의 아래 글을 참고한다.

예외 발생

  • 이제 시스템에서는 아래와 같이 예외를 발생시키면 된다.
throw new ApiException(

        "INVALID_CLIENT_ID",
        "The requested client identifier is invalid.",
        new ApiExceptionData().add("client_id", client_id)
);
저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, ExceptionHandler, 전역 예외 처리 로직 작성하기

개요

Spring Framework 3.2부터 @ControllerAdvice가 소개되었다. 이를 통해 우리는 특정 컨트롤러에 종속되지 않은 전역 범위에서 발생하는 특정 예외에 대한 처리를 한 곳에서 작성할 수 있게 되었다. 본 글에서는 이 방법을 설명하고자 한다.

목표

  • 특정 컨트롤러에 종속되지 않은 전역 예외 처리 로직을 작성한다.

사전 지식

특정 예외를 처리하는 ExceptionHandler 작성

@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SomeExceptionHandler {

    @ExceptionHandler(SomeException.class)
    @ResponseBody
    public ResponseEntity<?> handleSomeException(HttpServletRequest request, SomeException ex) {

        // SomeException 예외 발생시 처리 로직 작성
        return new ResponseEntity<>(ex, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseEntity<?> handleException(HttpServletRequest request, Exception ex) {

        // Exception 예외 발생시 공통 처리 로직 작성
        return new ResponseEntity<>(ex, HttpStatus.BAD_REQUEST);
    }
}
  • @ControllerAdvice를 클래스 레벨에 부여하면 @Controller 빈을 돕는 빈으로 기능할 수 있다. 예를 들어 모든 컨트롤러 제어 범위에서 특정 예외가 발생하면 공통적인 처리 로직을 작성할 수 있다.

  • @Order를 클래스 레벨에 부여하면 @ControllerAdvice 빈 간의 우선순위를 조정할 수 있다. @ControllerAdvice 빈이 여러 개일 때 어떤 것을 우선할지 결정할 수 있다. Ordered.HIGHEST_PRECEDENCE는 최우선 순위를 의미한다.

  • @ExceptionHandler를 메써드 레벨에 부여하면 특정 예외가 발생했을 때의 처리 로직을 작성할 수 있다.

  • @ResponseBody를 메써드 레벨에 부여하면 특정 뷰가 아닌 메시지 바디를 반환할 수 있다. 즉, REST API의 응답 형태로 예외 처리 로직의 결과를 반환하겠다는 것이다.

  • ResponseEntity 오브젝트는 HTTP 응답 코드, 응답 헤더, 응답 바디 모두를 담을 수 있는 만능 객체이다. 즉, REST API의 응답을 자유롭게 담을 수 있다.

특정 예외 발생 예

@Service
public class SomeService {

    public void doSomeBusinessLogic {

        // 특정 조건에 따라 SomeException 예외 발생
        throw new SomeException("SomeError Occured.");
        ...
    }
}
  • @Controller에 의해 호출되는 모든 범위에서 위와 같이 SomeException 예외를 발생시키면 앞서 작성한 @ControllerAdvice 빈에 의해 해당 예외에 대한 특정 예외 응답을 수행할 수 있다.

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring, MyBatis, Domain, DAO, Mapper XML 작성하기

목표

  • 데이터베이스 테이블과 맵핑되는 Domain, DAO, Mapper XML 파일을 작성하여 CRUD를 수행할 수 있다.

사전지식

Domain 클래스 작성

@NoArgsConstructor
@Setter
@ToString
public class User extends Paginator {

    @Getter
    private Long id;

    @Getter
    private String name;

    @Getter
    private Integer age;

    @Getter
    private LocalDateTime createdAt;

    @Getter
    private LocalDateTime updatedAt;

    @Getter
    private LocalDateTime deletedAt;

    @Getter
    private boolean isDeleted;
}

Mapper XML 작성

  • MyBatis가 다른 데이터베이스 관련 라이브러리보다 우위를 점하는 점은 바로 Dynamic SQL를 다룰 수 있는 자유도이다. OOP 세계인 Java 생태계에서 SQL을 자유롭게 다루기란 힘들다. MyBatisDAO 클래스는 그대로 Java의 손에 맡기고 Dynamic SQL을 생성하고 도메인 클래스에 맵핑하는 역할은 Mapper라 불리우는 XML 파일에 위임해버렸다. 이 Mapper 파일에서 로우 레벨의 쿼리 작성이 발생한다.
  • DAO 클래스에서 XML으로 로우 레벨을 위임하는 것은 장점만 가지는 것은 아니다. 많은 부분이 XML에 정의되어 있다 보니 개발자의 오타가 발생할 경우 런타임 단계에서야 발견된다. 컴파일 단계에서는 발견할 수 없다.
  • 아래는 name, age와 기타 공통 컬럼을 가진 users란 테이블의 조회 쿼리를 Mapper 파일로 작성한 예이다. Mapper 파일의 위치는 \src\main\resources\mapper\UserDAO.xml이다. 파일의 이름은 1:1로 대응되는 DAO 클래스와 일치시킨다. 이런 규칙을 지켜야 유지보수가 편리하다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.jsonobject.example.dao.UserDAO">

    <resultMap type="com.jsonobject.example.domain.User" id="userMap">
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="createdAt" column="created_at"/>
        <result property="updatedAt" column="updated_at"/>
        <result property="deletedAt" column="deleted_at"/>
        <result property="isDeleted" column="is_deleted"/>
    </resultMap>

    <select id="selectUserList" parameterType="com.jsonobject.example.domain.User" resultMap="userMap">
        SELECT id, name, age, created_at, updated_at, deleted_at, is_deleted
        <if test="limit != null and offset != null">
            , COUNT(0) AS total_count
        </if>
        FROM users
        <where>
            <if test="id != null">
                AND id = #{id}
            </if>
            <if test="name != null">
                AND name = #{name}
            </if>
        </where>
        ORDER BY created_at DESC
        <if test="limit != null and offset != null">
            LIMIT #{limit}
            OFFSET #{offset}
        </if>
    </select>

</mapper>
  • users라는 테이블의 각 로우를 user라는 리소스 단위로 생각할 수 있다. 나는 Mapper를 설계할 때 하나의 리소스 만을 조회하는 selectOne 로직은 작성하지 않는다. 리소스의 목록을 조회하는 selectList 메써드 만을 정의한다. 개별 리소스는 해당 메써드에 id 파라메터만 전달하면 획득할 수 있다. 이런 규칙으로 Mapper를 작성하면 중복되는 구문을 최소화할 수 있다.

DAO 클래스 작성

@Repository
@Slf4j
public class ResourceDAO {

    @Autowired
    @Qualifier("sqlSessionTemplate")
    private SqlSession sqlSession;

    public List<User> getUserList(User user) {

        return sqlSession.selectList("com.jsonobject.example.dao.UserDAO.selectUserList", user);
    }

    public User getUser(Long id) throws Exception {

        User params = new User();
        params.setId(id);
        List<User> userList = getUserList(params);
        if (userList.size() == 0) {
            return null;
        }

        return userList.get(0);
    }
}

SELECT: OneToMany

<resultMap type="com.jsonobject.example.domain.User" id="userMap">
    ...
    <collection column="id" property="UserPetList" select="selectUserPetList"/>
</resultMap>
  • @OneToMany는 1:N 관계를 나타낼 때 쓰이는 JPA의 어노테이션이다. 위 예제의 경우 한 명의 User가 여러 개의 Pet(애완동물)을 가질 경우 OneToMany에 해당한다.

  • MyBatis에서는 이러한 관계를 collection 엘러먼트에 관련 내용을 작성하면 된다. 주의할 점은 collection 엘러먼트를 모든 result 엘러먼트를 작성한 후 기입해야 한다. 순서를 지키지 않으면 구문 오류가 발생한다.

  • 해당 쿼리문 실행시 MyBatis는 해당 userid 컬럼을 조회키로 selectUserPetList 메써드를 실행(user_pets 테이블에서 N개 로우를 조회)하여 userMap에 맵핑해준다. 개발자 입장에서는 상당히 편리하다.

  • 성능 관점에서 단일 로우 조회라면 큰 문제가 없다. 하지만 복수 개의 로우를 조회한다면 매 로우보다 collection을 조회하는 쿼리가 추가로 실행되는 N+1 문제가 발생한다. 이 문제를 해소하기 위해 각 데이터가 실제 참조될 때에만 로드하는 Lazy Loading 기법을 사용한다. fetchType="lazy" 애트리뷰트 설정을 추가하면 해당 collection에는 Lazy Loading이 적용된다. 해당 엔티티에서 getter 메써드가 호출될 때 비로소 실제 쿼리가 실행되는 것이다. (기술적으로는 해당 메써드를 감싸는 프록시 기법이 사용된다.)

INSERT

<insert id="insertUser" parameterType="com.jsonobject.example.domain.User">
    INSERT INTO users
    <set>
        name = #{name},
        age = #{age},
    </set>
    <selectKey keyProperty="id" resultType="long" order="AFTER">
        SELECT LAST_INSERT_ID()
    </selectKey>
</insert>
  • MySQL, MariaDBLAST_INSERT_ID() 함수로 현재 연결에서 가장 마지막의 삽입된 PK를 획득할 수 있다. MyBatis에서는 selectKey 엘러먼트에 관련 내용을 작성하면 된다.
  • 위 예제의 경우 INSERT 쿼리문 실행 후 LAST_INSERT_ID() 반환 값을 User 도메인 클래스의 id 프라퍼티에 저장한다.
public long createUser(User user) {

    sqlSession.insert("com.jsonobject.example.dao.UserDAO.insertUser", user);

    return user.getId();
}
  • 앞서 Mapper XML에서 작성한 내용은 Java DAO에서 위와 같이 맵핑하여 작성한다. 명령이 정상적으로 실행되면 새롭게 생성된 users 로우의 id를 반환한다.

DELETE

<delete id="deleteUser" parameterType="com.named.api.domain.User">
    <if test="id != null">
        DELETE FROM users
        WHERE id = #{id}
    </if>
</delete>
  • DELETE 쿼리문은 가장 간단한다. 위 예제의 경우 안전 장치로 id 프라퍼티에 값이 없을 경우 쿼리를 실행하지 않게 조치했다.
public int deleteUserById(String id) {

    User params = new User();
    params.setId(id);

    return sqlSession.delete("com.jsonobject.example.dao.UserDAO.deleteUser", params);
}
  • 명령이 정상적으로 실행되면 삭제된 로우 수를 반환한다. 위 예제의 경우 1을 반환한다.
저작자 표시 비영리 동일 조건 변경 허락
신고

PES 2017, 마스터리그 팀 추천, 레알 마드리드

레알 마드리드

  • PES 2017 + PTE Patch 5 조합은 2016/2017 시즌의 레알 마드리드 팀을 담고 있다. 2016/2017 시즌의 레알 마드리드는 지네단 지단 감독의 2년차 지도력이 꽃을 피우며 프리메라리가와 챔피언스리그를 우승한 명실공히 최강팀이라고 할 수 있다.
  • 2017/2018 시즌의 레알 마드리드를 즐기고 싶으면 PTE Patch 6를 설치하면 된다. 알바로 모라타의 첼시 이적 등이 반영되어 있다.

포메이션



  • 레알 마드리드는 빠른 측면 역습을 특징으로 하는 4-3-3 전술을 구사한다. 주전 선수들의 면면이 화려하다.
  • 2016/2017 시즌의 레알 마드리드가 가지는 또다른 강점은 막강한 로테이션이다. 주전 선수 한두명이 결장해도 막강한 서브 선수들이 대기하고 있다.

크리스티아누 호날두



  • 메시와 더불어 현존 최고 스타인 호날두이다. 그의 발재간을 PES 2017에서도 고스란히 즐길 수 있다.

카림 벤제마


  • 벤제마는 스스로도 일류 공격수이면서 호날두와의 연계 플레이도 좋아 상당한 파괴력을 보여준다.
  • 벤제마의 서브로는 벤제마 못지 않은 활약이 가능한 알바로 모라타가 버티고 있다.

가레스 베일


  • 치달의 달인 가레스 베일이다. 호날두와 동시 기용시 오른쪽 측면에서 중앙으로 휘젓는 플레이는 가공할만한 파괴력을 보여준다.
  • 가레스 베일의 서브로는 특출나지는 않지만 언제나 평타 이상의 균형감 있는 플레이를 보여주는 루카스 바스케스가 버티고 있다.


저작자 표시 비영리 동일 조건 변경 허락
신고

Java, UUID 생성하기

목표

  • UUID v1부터 v4까지 다양한 방식의 UUID를 생성할 수 있다.
  • 생성한 UUIDURL에 포함 가능한 22개 문자열로 변환할 수 있다.
  • API 업계에서 흔히 쓰이는 UUID 표현 방식은 UUIDurl62 방식으로 변환하는 것이다. url62UUID 바이트 배열을 Base 62([a-zA-Z0-9] 62개 문자만을 사용) 문자열 변환하는 것이다.

라이브러리 종속성 추가

dependencies {
    compile group: 'com.fasterxml.uuid', name: 'java-uuid-generator', version: '3.1.4'
    compile group: 'commons-codec', name: 'commons-codec', version: '1.10'
}
  • java-uuid-generator(Apache 2.0)는 RFC 4122에 명시된 모든 방식의 UUID 생성 기능을 제공한다.

  • commons-codec(Apache 2.0)은 바이트 배열을 URL에 포함 가능한 22개 크기의 문자열로 변환하는 기능을 제공한다.

  • 한편, url62 방식을 사용하고자 한다면 glowfall/base62(Apache 2.0)를 사용할 수 있다. 메이븐 저장소에 없기 때문에 복사하여 사용해야 한다.

UUID 생성하기

private String generateUUID() {

    // RFC 4122 variant 2, version 1 방식으로 생성된 UUID를 반환
    TimeBasedGenerator uuidV1Generator = Generators.timeBasedGenerator(EthernetAddress.fromInterface());
    UUID uuid = uuidV1Generator.generate();

    // 또는 RFC 4122 version 4 방식으로 생성된 UUID를 반환
    uuid = Generators.randomBasedGenerator().generate();

    // URL에 포함될 수 있는 Base64 문자열로 변환
    ByteBuffer uuidBytes = ByteBuffer.wrap(new byte[16]);
    uuidBytes.putLong(uuid.getMostSignificantBits());
    uuidBytes.putLong(uuid.getLeastSignificantBits());

    return org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(uuidBytes.array());
}
  • 한 번 생성된 UUID 객체는 toString()을 사용할 경우 32개 크기의 문자열로 변환할 수 있다. Apache Commons Codec이 제공하는 Base64 클래스를 이용하면 동일한 원자성을 유지하면서 URL 친화적인 22개 크기의 문자열을 생성할 수 있다. 결과는 아래와 같다.
Hsxpe15HTAmMaXteR3wJhA
YcrgST0DRf6K4Ek9A6X-dg
LyiTKOHpQPGokyjh6YDxAg
ZFqTy_xJRm2ak8v8SWZtdw
0cwWlpFrSDiMFpaRa7g48g
fHIjWcjyTLSyI1nI8ty0PA
ssrDdCCdSHyKw3QgnSh8ug
yde_mqRcSyWXv5qkXDslMQ
NepFt7mwR6uqRbe5sEercw
L_wLRdyVSqy8C0XclZqssQ

url62 변환 UUID 생성하기

private String generateUrl62UUIDv4 {

    UUID uuid = Generators.randomBasedGenerator().generate();

    ByteBuffer uuidBytes = ByteBuffer.wrap(new byte[16]);
    uuidBytes.putLong(uuid.getMostSignificantBits());
    uuidBytes.putLong(uuid.getLeastSignificantBits());

    return Base62.encode(uuidBytes.array());
}
  • 생성한 UUIDurl62 방식으로 변환하면 보다 안전한 문자열을 생성할 수 있다. client_id, authorization_code, access_token, refresh_token 등에 두루 사용할 수 있다.
BrIfqEKoPqVF5rSogettSH
OJ0wVlmILNhgDXVais0EyD
YNZShSzuEhzkJFKN7SLOWA
qBsv62QRENPgeV3GKiu5zH
mf0q7cwnGSsTruzBf0wi1E
XbtKb0gHDBvlqsRDeYY4HA
fHbFHfwQEU6Yq44DDRMpDI
x9LHxnqkFp9vcEfUlsCtBG
va9xZxtxCpllHnF3GLtW9C
v5Hau0qgIdqvo5SrCi2pmD
  • 한편, client_secret 값 또한 url62로 생성이 가능하다. 다만, 데이터베이스에 보관할 때는 BCrypt로 해쉬 변환된 값을 저장해야 한다. 사용 예는 아래와 같다.
// 텍스트 그대로를 반환한다. 테스트 환경에서만 사용해야 하며 절대 운영 환경에서는 사용하면 안된다.
NoOpPasswordEncoder.getInstance().encode(client_secret);

// BCrypt 방식의 해시로 변환하여 반환한다. 운영 환경에 가장 적합하다.
BCryptPasswordEncoder.getInstance().encode(client_secret);
  • 앞서 생성된 값들을 BCrypt 해시로 변환하면 아래와 같다. 60개 크기의 문자열이 생성된다.
$2a$10$C9dXz/q/ZE/nvOXFxG0Pvu4ZqWESlGXKtGyhkz5.q8s2OTSNyK/8y
$2a$10$7HZdG8.bvW08yoGpZZ9us.sFA2UANbivpWBo0/on4LemxGxpr946K
$2a$10$ENMG.zVPYjVujd.XHFIjkejo1fNrSchuks4NDFBxr0WfcRC5V12T.
$2a$10$A5OzbZBq9U2KPiAaeKEjx.bZgoF2uwSVoa3C2onVvPnfEqKMM1fDO
$2a$10$fBK8iyhC6tClKoHR8v/ZWOQRFMPkKXjmLquAHjpQLLwYjCQEc.K3q
$2a$10$L5EZAy4QltMKjtN6v8SD7esaMiqBy5MvdYg6gcRRDKET6e5vXjTpa
$2a$10$dN80n/LO5wLUi4peLfylO.UzJ4s4Xf0LWWehnV7//LjFB6e9OAz3a
$2a$10$WGuswZRmbgEKeAtgix78Y.oiXcz5H6pS0wrfC7XbKOiN6nD2cSjoa
$2a$10$7Z5zGcIIQraUzDNw4ZFyTeT0dBRu8fmsi54OiN9vMQZFIf30v3leu
$2a$10$YMzU80KrozWw4ABHj1ltiu735rARX1aC/owCkeYvcDbvVx5uxRTsa
저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, MyBatis, MySQL, MariaDB 레플리케이션 구현하기

목표

  • MySQL 또는 MariaDB의 레플리케이션(1개 마스터, n개 슬레이브)를 지원하는 하나의 DataSource 빈을 생성한다.

  • 앞서 생성한 DataSource 빈을 관리하는 PlatformTransactionManager 빈을 생성한다.

  • 앞서 생성한 DataSource 빈에 연결하는 MyBatisSqlSession 빈을 생성한다.

기대효과

  • 개발자가 직접 각 데이터베이스를 직접 바라보는 로우 레벨의 DataSource 구성을 관리할 필요 없이 평소와 동일하게 비즈니스 로직에 집중할 수 있다.

  • @Service 클래스의 각 메써드 레벨에 명시하는 @Transactional 어노테이션으로 마스터, 슬레이브로 향할 SQL 문을 결정할 수 있다. @Transactional(readOnly = true)이면 슬레이브로, @Transactional(readOnly = false)이면 마스터로 쿼리를 실행한다.

사전지식

DataSourceConfig

  • 아래와 같이 @Configuration 클래스를 작성하면 모든 준비가 완료된다. 기본 프로젝트 구성은 이 글을 참고한다.
package com.jsonobject.example.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mybatis.replication.datasource.ReplicationRoutingDataSource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class DataSourceConfig {

    @Primary
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.master.datasource")
    public DataSource masterDataSource() {

        return DataSourceBuilder.create().build();
    }

    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.slave.datasource")
    public DataSource slaveDataSource() {

        return DataSourceBuilder.create().build();
    }

    @Bean(name = "routingDataSource")
    public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource) {

        ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource(masterDataSource, null);
        routingDataSource.addSlave(slaveDataSource);

        return routingDataSource;
    }

    @Bean(name = "dataSource")
    public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {

        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {

        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource, ApplicationContext applicationContext) throws Exception {

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:mapper/*.xml"));

        return sqlSessionFactoryBean.getObject();
    }

    @Bean(name = "sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {

        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, JVM 옵션 설정하기

JVM 실행 옵션 예

$ java -Xms8g -Xmx8g -XX:MaxMetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+CMSClassUnloadingEnabled -server -Dspring.profiles.active=prod -jar app.jar
  • 힙 크기에 관한 어떤 설정도 하지 않을 경우 JVM은 물리 메모리의 1/6 크기를 최소 힙 크기(-Xms)에, 물리 메모리의 1/4 크기를 최대 힙 크기(-Xmx)에 할당한다. 물리 메모리가 32GB라고 가정하면 -Xms4g, -Xmx8g 정도를 할당하는 셈이다. [관련 링크]

  • JVM은 애플리케이션 시작시 -Xms로 설정한 값으로 힙 크기를 생성하고 요구량이 증가할 때마다 -Xmx로 설정한 값까지 점진적으로 힙 크기를 늘린다. 힙 크기를 늘리는 과정에서 애플리케이션이 일시적으로 멈추는 병목 현상(Stop the World Event)이 발생할 수 있다. 따라서 물리 메모리가 넉넉하다면 처음부터 -Xms 값을 -Xmx와 동일하게 설정하는 방법을 추천하기도 한다. [관련 링크 1] [관련 링크2]

  • 한편, 64-bit 환경에서 물리 메모리가 32GB를 훨씬 초과하는 시스템이라고 하더라도 -Xmx 옵션을 32GB 이하로 설정하는 것이 적절하다. 32GB를 초과하면 힙을 관리하는 방식이 비효율적인 방식으로 변경된다. [관련 링크 1] [관련 링크 2]

  • -XX:MaxMetaspaceSizePermanent Generation 영역을 의미한다. 과거 -XX:MaxPermSize 용어로 쓰이다가 Java 8에 와서 명칭이 바뀌었다. 값을 지정하지 않으면 기본 값으로 64MB가 사용되는데 일반적인 엔터프라이즈 용도라는 낮은 수치이다. 오라클에서는 256MB로 설정할 것을 권장한다.

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

드미트리렌더(DmitriRender) 설치 및 구매하기

라이센스 구매

  • DmitriRender는 비자 또는 마스터카드 결제가 가능한 신용카드만 있으면 온라인으로 구매가 가능하다. 가격은 USD 8.80(약 10,000원)이다. 구매 과정은 국내 인터넷 쇼핑몰만큼 간단하다. 방법은 아래와 같다.
1. 공식 홈페이지에 접속한다. (http://www.dmitrirender.ru/)
2. Buy 메뉴로 이동 후 buy DmitriRender activation key 링크를 클릭한다.
3. 라이센스 키를 받을 이메일 주소(중요), 이름, 성, 주소, 우편번호, 시, 국가, 결제 방법(중요, 신용카드/페이팔 택일)을 입력하고 결제를 완료한다.
4. 결제가 끝나면 바로 앞서 입력한 이메일 주소로 XX-XXXXXX-XXXXXX-XXXXXX-XXXXXX 형식의 라이센스 키가 발송된다. 잃어버리지 않게 잘 보관한다.
5. Windows 시작 메뉴에서 DmitriRender > License Manager를 실행하고 라이센스 키를 적용한다.







  • DmitriRender는 한 번 구매하면 최대 2개 PC에서 사용 가능한 라이센스 키를 제공한다.
  • 각 PC에 적용한 라이센스 키는 사용하지 않을 경우 반드시 해제해야 다른 PC에서 사용이 가능하다. 해제하지 않고 PC를 포맷하면 라이센스 키를 사용할 수 없게 된다.


저작자 표시 비영리 동일 조건 변경 허락
신고

REST API와 국제화 (Internationalization, i18n)

국제화

  • 작년에 미국, 캐나다 국민을 대상으로 하는 REST API 서비스를 설계한 적이 있다. 당시 고객의 요구사항은 파라메터 입출력에 있어 영어 뿐만 아니라 프랑스어도 지원해달라는 요구가 있었다. 이유는 캐나다의 퀘벡 주는 프랑스어를 사용하기 때문이다. (유명 팝 가수인 셀린 디옹이 퀘벡 출신으로 프랑스어를 구사한다.)

  • 이러한 국제화를 i18n이라고 부른다. 뭔가 거창해 보이고 욕(?) 같아 보이기도 하지만 진짜 의미는 굉장히 단순하다. 그냥 Internationalization의 맨 앞글자 i와 맨 뒷글자 n 사이의 글자수가 18개라서 줄여서 i18n이라고 부르는 것이다.
  • 서비스의 국제화 범위는 단순히 메시지에 대한 다국어 지원을 하는 것에서 부터 각 국가의 통화, 날짜 등을 표현하는 것까지 다양하다.

  • 국제화를 적용하려면 각 국가와 언어권을 식별할 수 있는 코드가 필요하다. 대표적으로 Language Tags이 널리 쓰인다. 미국을 예로 들면 en-US, 대한민국은 ko-KR로 부르는 식이다. 모든 영어권을 통틀어 en으로 축약하기도 한다. [관련 링크]

REST API의 국제화 적용

  • HTTP 기반의 REST API에 국제화를 적용하는 방법은 매우 간단하다. 요청 헤더에는 Accept-Language를, 응답 헤더에는 Content-Language를 추가하면 된다. 값으로는 앞서 언급한 Language Tags를 입력한다.
// 요청 헤더
Accept-Language: en

// 응답 헤더
Content-Language: en
  • 요청 헤더를 생략할 경우 서버 측에서 기본 언어권을 설정해두는 것도 필요하다. (ex: i18n.default_locale = en)

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고