2017년 9월 11일 월요일

JPA에 @Convert 사용법

밑도 끝도 없이 예제를 작성한다.
하고 싶은 건 DB의 field에 Json으로 저장하고 조회 시 이를 class로 받고 싶은 경우다.

코드는 kotlin으로 되어 있다.

Reservation 테이블에 Remark를 Json으로 저장하고 조회 시 Remark Class로 다시 변환해서 가져온다.

Entity Class

import javax.persistence.Convert
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id

@Entity
@Table(name = "reservation")
class Reservation (
    var arrival: LocalDate,
    var departure: LocalDate
){
    @Id
    @GeneratedValue
    var id: Long? = null
        private set
    @Convert(converter = JpaJsonRemarkConverter::class)
    var remarks: MutableList? = null
}

Remark Class

data class Remark(
        var id: String,
        var name: String,
        var rate: Double
)

JpaJsonRemarkConverter Class

class JpaJsonRemarkConverter : AttributeConverter<MutableList<Remark>, String> {
    private val gson = Gson()

    override fun convertToEntityAttribute(dbData: String?): MutableList<Remark>? {
        if (dbData == null)
            return null

        val collectionType = object : TypeToken<MutableList<Remark>>() {}.type

        return gson.fromJson(dbData, collectionType)
    }

    override fun convertToDatabaseColumn(attribute: MutableList<Remark>?): String? { 
            if (attribute == null)
            return null

        return gson.toJson(attribute)
    }
}

json으로 변환은 gson으로 했는데 jackson이 spring 기본 json parser니 그걸 써도 된다.

저러면 Remark Class에 데이터를 넣고 save하면 DB에는 json 형태로 들어가고 조회시 Remarks:List 형태로 된다.

2017년 6월 19일 월요일

Intellij saving caches 느린 것 해결 방법.

개요

Intellij의 Debug를 돌리면 프리징 현상이 일어나는 경우가 있는데 이에 대한 해결책이다.


해결 방법

검색을 하면 /etc/hosts에 {hostname}.local로 붙이라고 되어 있는데 이렇게 해도 안되는건 hostname을 잘못 알고 있을 가능성이 크다.

git clone https://github.com/thoeni/inetTester

java -jar ./bin/inetTester.jar

Calling the hostname resolution method...
Method called, hostname Seoui-MacBook-Pro.local, elapsed time: 5006 (ms)


저기서 Seoui-MacBook-Pro.local을 /etc/host에 등록하면 된다.

127.0.0.1 localhost Seoui-MacBook-Pro.local
255.255.255.255 broadcasthost
::1             localhost Seoui-MacBook-Pro.local



출처: https://intellij-support.jetbrains.com/hc/en-us/articles/206544799/comments/115000058504

2017년 2월 22일 수요일

SpringBoot Data Cache 적용하기

Springboot + Spring data cache를 적용하는 방법이다.
적용 할 때 특정 cache는 ttl을 별도 지정하고 key(String)를 눈으로 확인 할 수 있도록 String 형태로 구성한다.

일단 Configuration을 작성하고..
(cache ttl을 별도 옵션으로 뺐기 때문에 CacheExpireProperties class를 이용해서 yml의 properties를 load한다)


@EnableCaching
@AutoConfigureAfter(CacheAutoConfiguration.class)
@Configuration
public class CacheConfiguration extends CachingConfigurerSupport {
    @Autowired
    private CacheExpireProperties cacheExpireProperties;

    @Override
    public KeyGenerator keyGenerator() {
        return new CustomKeyGenerator();
    }

    @Override
    public CacheErrorHandler errorHandler() {
        return new CustomCacheErrorHandler();
    }

    @Bean
    @Primary
    public RedisTemplate redisTemplate(
            RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new GenericToStringSerializer(String.class));

        return template;
    }


    @Bean
    public CacheManagerCustomizer cacheManagerCustomizers() {
        return (CacheManagerCustomizer) cacheManager -> {
            cacheManager.setExpires(cacheExpireProperties.getExpires());
        };
    }

}
cache에 key로 쓸 keyGenerator를 만들자

public class CustomKeyGenerator implements KeyGenerator{

    private static final String delimiter = ":";

    @Override
    public Object generate(Object target, Method method, Object... params) {
        return generate(params);
    }

    public Object generate(Object... params) {
        StringBuilder sb = new StringBuilder();
        sb.append("sample");
        if(Objects.nonNull(params) && params.length > 0){
            sb.append(delimiter);

            for(int i = 0; i < params.length ; i++){
                sb.append(params[i].toString());

                if(i < params.length - 1)
                    sb.append(delimiter);
            }
        }

        return sb.toString();
    }
}
cache를 처리하다가 exception이 난 경우 이를 처리 하는 Handler도 만들자

public class CustomCacheErrorHandler implements CacheErrorHandler{
    @Override
    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
        log.error("cache not get");
    }

    @Override
    public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
        throw exception;
    }

    @Override
    public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
        throw exception;
    }

    @Override
    public void handleCacheClearError(RuntimeException exception, Cache cache) {
        throw exception;
    }
}
ttl을 별도 적용 할 cacheName 및 설정값을 읽자

@Data
@Component
@ConfigurationProperties(prefix = CacheExpireProperties.PREFIX)
public class CacheExpireProperties {
    public static final String PREFIX = "sample.cache";

    private Map expires = new HashMap<>();
}
application.yml에 아래처럼 설정하자

sample:
  cache:
    expires:
      foods: 300
간단히 어노테이션만 달면.. 완성!

    @Cacheable("foods")
    public Food findFood(long storeId, long foodId){
       ...
    }

2017년 1월 31일 화요일

DSYM Info.plist

Xcode에서 앱 빌드를 하게 되면 {BundleName}.app.DSYM 폴더가 생긴다.
이 안에는 Contents/Info.plist와 /Contents/Resources 에 Symbol 파일 2개가 있다.

여기서 Info.plist는 symbolication에서 필요한 필수 값을 몇개 가지고 있다.
Info.plist를 열어보면


<plist>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleIdentifier</key>
<string>com.apple.xcode.dsym.net.daum.mf.sample.mobilereportlibrary</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>dSYM</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>0.13</string>
<key>CFBundleVersion</key>
<string>0.13</string>
</dict>
</plist>


형태로 구성되어 있다.

위 값을 설명하면 다음과 같다.

<key>CFBundleIdentifier</key>
<string>com.apple.xcode.dsym.net.daum.mf.sample.mobilereportlibrary</string>
bundleIdentifier를 나타내며 com.apple.xcode.dsym. 을 빼면 앱의 identifier를 나타낸다.


<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
해당 Info.plist의 버전을 의미한다


<key>CFBundleShortVersionString</key>
<string>0.13</string>
앱의 Bundle Version을 의미한다.


<key>CFBundleVersion</key>
<string>0.13</string>
앱의 Build Version을 의미한다.


저 Info.plist를 직접 파싱해서 값을 꺼낼 수도 있지만 PlistBuddy를 이용해서 값을 추출 할 수 있다.
예를 들어 bundleIdentifier를 추출하고 싶은 경우
bundleIdentifier -c "Print:CFBundleIdentifier" Info.plist
이렇게 하면 값이 추출된다.

Builder Pattern에 대해서

자바 개발자라면 대부분 아는 Builder Pattern을 굳이 설명 할 필요는 없을 것 같고 단지 개발을 하면서 이 패턴이 매우 유용하게 쓰이는 부분들이 있어서 끄적여 본다.

위키피디아에 있는 예제를 보면 영문과 국문이 상이하다.
국문 위키피디아의 Builder Pattern java 예제는 PizzaBuilder를 상속받은 HawaiianPizza와 SpicyPizza를 Cook을 이용해서 Pizza를 만들어내는(??) 방법으로 빌더 패턴을 설명하고 있다.
(개발자라서 글재주가 없으니..;; 그냥 소스를 보도록 하자..ㅠㅠ)

영어 위키피디아의 Builder Pattern java 예제는 StreeMap이란 객체를 생성할 때 inner Class인 Builder를 이용하는 방법을 설명하고 있다.
일단.. 내가 이야기 할 부분은 후자인 영어 위키피디아에서 빌더패턴으로 객체를 만들어내는 부분이다.
이 패턴은 Value Object를 생성할 때 매우 유용하다.

과거 빌링 플랫폼을 개발하면서 정말 힘들었던 점은, 입력받는 파라미터의 숫자가 너무 많은데다가 변수 이름도 굉장히 어려웠다.
(payment와 settlement는 다른 의미고.. 현금성, 비현금성 금액을 영어로 하기도 어렵고..)
그리고 법은 자꾸 바뀌어서 이러랬다 저러랬다 해서 소스도 매우 더러운 편이었고,
돈을 다루는 곳이다보니 10원가지고 엄청난 CS도 들어오기도 했다.
이중에 제일 힘든 부분 12년이 넘게 운영되다보니 테이블의 컬럼도 무지 많은데 도대체 어떤게 쓰이고 안쓰이는지 잘 모르는 상태에다가 비즈니스 로직마다 입력을 해야 하는 값들이 아주 조금씩 달라서 배포하고 나면 장애가 종종 나기도 했다. 더군다나 빌링은 워낙 오랜 새월 여러 개발자들은 손을 거치다보니 왜 개발이 이렇게 되었는지 히스토리를 알수도 없었다.

해서 이런 문제들을 해결 하기 위한 하나의 방법으로 Value Object를 생성할 때 몇가지 룰을 적용하였고 이로 인해서 전보다는 가독성있는 소스가 만들어졌다.
PL을 할 때 팀원들과 정했던 룰인데 요기에 정리해본다.


StreetMap map = new StreetMap.Builder(new Point(50, 50), new Point(100, 100))
       .landColor(Color.GRAY)
       .waterColor(Color.BLUE.brighter())
       .build();

위의 위키피디아 예제를 가지고 설명하도록 하도록 하겠다.


public Builder(Point origin, Point destination) { 
        this.origin      = origin; 
        this.destination = destination; 
}

1. Builder 생성자에는 필수값 파라미터만 사용한다. 따라서 null을 허용하지 않는다.
이렇게 정한 가장 큰 이유는 Builder로 선언될 때 무조건 input value가 있어야 하기 때문에 객체를 생성 할 때 실수로 파라미터 누락이 생기는 걸 방지하기 위함이다. 만약 setter를 쓰게 되면 origin을 누락시키거나 destination을 누락시킬 수 있기 때문에 이를 객체 생성시 부터 막도록 한다.


public Builder landColor(Color color) {
         landColor = color; return this; 
}

2. Builder를 리턴하는 메소드는 각각의 비즈니스에 사용되는 파라미터임을 알 수 있도록 명명한다.
우리가 사용한 방법은 public Builder는 기본적으로 결제 시 사용하는 파라미터들을 담게 하고 정기결제 같은 경우 옵션형태이기 때문에 메소드로 빼서 객체를 생성 할 때 구분하도록 했다.


//일반 결제
public Builder (Payment payment, User user) {...} 

//정기결제 
public Builder subscription(int term, long item, String message) {...}

3. 객체에 Setter 형태로 내부 변수값을 변경 할 수 있는 것은 가능한 만들지 않는다.
객체가 한번 선언되면 내부 변수 값이 외부에 의해서 변경되지 않도록 하는 것은 결제에서 매우 중요하다.사실 변경될 여지도 거의 없을 뿐더러 변경이 가능하다면 분명한 이유가 존재해야 한다. Setter 자체를 생성하는 건 요즘 IDE가 너무 좋아서 그냥 짠 하고 만들어지지만 제공해선 안되는 변수까지도 개발자의 실수로 생성할 수 있다 . (최소한 아무나 만들 수 있고 실수로 만들 수 도 있으니..)이는 로직이 복잡하거나 빌링처럼 오만가지 파라미터나 난무하는 경우에 객체에 어떤 것이 접근 가능하고 불가능한지를 알려면 소스를 다 보거나 그 비즈니스를 모두 이해해야 한다.
해서 우리는 가능한 모든 변수는 private final로 선언하는 것을 원칙으로 삼고 상태를 나타내는 값이거나 변경되어도 객체가 갖는 비즈니스에 전혀 영향을 주지 않는 값이라고 생각되는 변수만 setter를 허용하였다.


private final Payment payment;
private String message;
...

public void setMemo(String message){
    this.message = message
}


이런 빌더패턴을 가지고 VO 객체를 생성하는 방법은 빌링 처럼 오래된 시스템에 적용하기 참 좋다. 가독성을 높히는 부분도 있으며 개발자의 실수를 줄일 수 있고 시스템을 크게 변화시키기 않는다. 그리고 복잡한 비즈니스 로직 일부를 가시성있게 만들어 줄 수도 있다.
즉 정답이라고 볼 수는 없지만 현실적으로 좋은 방법이라고 이야기 하고 싶다.

기본 결제에 대한 예제 Value Object 예제


package io.daumkakao.fennec.vo;

import java.util.Date;

/**
 * Created by elijah17
 * 직접 결제
 */
public class DirectPayment{

    private final User user;
    private final Item item;
    private final Price price;
    private final PaymentType paymentType;

    private final Subscription subscription;
    private final int period;
    private final Date startDate;

    private final FreeType freeType;
    private final int freePeriod;

    private final ReceiptType receiptType;
    private final Receipt receipt;

    private String memo;



    public static class Builder {

        private final User user;
        private final Item item;
        private final Price price;
        private final PaymentType paymentType;

        private Subscription subscription;
        private int period;
        private Date startDate;
        private FreeType freeType;
        private int freePeriod;

        private ReceiptType receiptType;
        private Receipt receipt;
        private String memo;



        public Builder(PaymentType paymentType, User user, Item item, Price price){
            this.paymentType = paymentType;
            this.user = user;
            this.item = item;
            this.price = price;
        }

        /**
         * 자동 결제
         * @param subscription 정기결제
         * @param period 기간
         * @param startDate 정기결제 시작일
         * @return
         */
        public Builder autoPay(Subscription subscription, int period, Date startDate){
            this.subscription = subscription;
            this.period = period;
            this.startDate = startDate;
            return this;
        }

        /**
         * 무료 사용기간
         * @param freeType 무료 타입
         * @param freePeriod 무료 기간
         * @return
         */
        public Builder freePolicy(FreeType freeType, int freePeriod){
            this.freeType = freeType;
            this.freePeriod = freePeriod;
            return this;
        }

        /**
         * 영수증 증빙 처리
         * @param receipt 영숭증 정보
         * @return
         */
        public Builder receipt(ReceiptType receiptType, Receipt receipt){
            this.receiptType = receiptType;
            this.receipt = receipt;
            return this;
        }

        public Builder memo(String memo){
            this.memo = memo;
            return this;
        }

        public DirectPayment build(){
            return new DirectPayment(this);
        }
    }

    private DirectPayment(Builder builder){
        user = builder.user;
        item = builder.item;
        price = builder.price;
        paymentType = builder.paymentType;
        subscription = builder.subscription;
        period = builder.period;
        startDate = builder.startDate;
        freeType = builder.freeType;
        freePeriod = builder.freePeriod;
        receiptType = builder.receiptType;
        receipt = builder.receipt;
        memo = builder.memo;
    }

    public User getUser() {
        return user;
    }

    public Item getItem() {
        return item;
    }

    public Price getPrice() {
        return price;
    }

    public PaymentType getPaymentType() {
        return paymentType;
    }

    public Subscription getSubscription() {
        return subscription;
    }

    public int getPeriod() {
        return period;
    }

    public Date getStartDate() {
        return startDate;
    }

    public FreeType getFreeType() {
        return freeType;
    }

    public int getFreePeriod() {
        return freePeriod;
    }

    public ReceiptType getReceiptType() {
        return receiptType;
    }

    public Receipt getReceipt() {
        return receipt;
    }

    public String getMemo() {
        return memo;
    }

    public void setMemo(String memo) {
        this.memo = memo;
    }
}


Value Object를 가지고 객체 생성하는 예제

// 직접결제
DirectPayment directPayment = new DirectPayment.Builder(PaymentType.CREDIT, user, item, price).build();

//정기 결제 인 직접 결제
DirectPayment directPayment = new DirectPayment.Builder(PaymentType.CREDIT, user, item, price).autoPay(subscription, 12, new Date()).build();


Gson TypeToken 사용하기

공통 포멧에 특정 Key에 대해서만 포멧 변경이 일어나는 Json에 대해서 Parse하는 방법에 대해서 적어본다.

일단 공통 포멧이 있는 대표적인 Json 예제를 들자면

{"status":"0000","message":"is ok", "resultData":{"seq":"1", "name":"5dolstory"}}
{"status":"0000","message":"is ok", "resultData":{"target":"mobile", "type":"credit"}}

위와 같은 형태인 API라고 보면 된다.
field를 보면 status, message는 항상 같고 resultData의 value field만 변경되는 경우이다.
이때 어떻게 객체로 변화시킬 수 있을까?


public class NameRes {
private String status;
private String message;
private Name resultData;
}

public class Name{
private int seq;
private String name;
}

public class TargetRes {
private String status;
private String message;
private Target resultData;
}

public class Target {
private String target;
private String type;
}

위와 같이 NameRes, TargetRes에 각각 status, message value를 만들고 이걸 parse해도 문제는 없다.
(apiResult는 String 타입의 response이라고 생각하자)

NameRes nameRes = new Gson().fromJson(apiResult, NameRes.class);
TargetRes targetRes = new Gson().fromJson(apiResult, TargetRes.class);

만약 여기서 Target Class만 혹은 Name Class만 parse하려면 어떻게 해야 할까?
방법은 여러가지인데 요기선 TypeToken으로 하는 방법을 쓴다.


public class Res {
private String status;
private String message;
private JsonElement resultData;
}

public class Name {
private int seq;
private String name;
}

public class Target {
private String target;
private String type;
}

//이걸로 공통값을 뽑아서 status 검증을 하자.
Res res = new Gson().fromJson(apiResult, Res.class);
if(res.getStatus())
...

Type nameType = new TypeToken<>(){}.getType(); //결과가 list인 경우
List names = new Gson().fromJson(res.getResultData(), nameType);

위처럼 하면 좋은 점이 공통적인 res 검증코드하고 실제 필요한 객체를 구분해서 처리할 수 있고 Name 객체 안에는 Name class에 맞는 변수들만 들어가 있게 된다.

JSR-303 (bean validation) and validating collection for Spring


SpringBoot를 쓰면 RequestBody나 ModelAttribute로 Json을 객체에 그대로 바인딩하면서 유효성 체크까지 할 수 있다.

이때 기본적인 Validator는 Collection 형태의 객체는 유효성을 체크하지 않는데 이걸 해주게끔 Validator를 구현했다.


import org.springframework.beans.BeanUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;


public class JSR303CollectionListValidator implements Validator {

    private final Validator validator;

    public JSR303CollectionListValidator(LocalValidatorFactoryBean localValidatorFactoryBean) {
        this.validator = localValidatorFactoryBean;
    }

    @Override    
    public boolean supports(Class clazz) {
        return clazz.isAssignableFrom(clazz);
    }

    @Override    
    public void validate(Object target, Errors errors) {
        ValidationUtils.invokeValidator(validator, target, errors);

        if(!target.getClass().isPrimitive()){
            PropertyDescriptor[] propertyDescriptors  = BeanUtils.getPropertyDescriptors(target.getClass());

            for(PropertyDescriptor descriptor : propertyDescriptors){
                if(List.class.isAssignableFrom(descriptor.getPropertyType())){
                    Method method = descriptor.getReadMethod();

                    try {
                        Object object = method.invoke(target);

                        if(!Objects.isNull(object)){
                            List typeOfObject = (List) object;

                            for(Object resource : typeOfObject){
                                validate(resource, errors);
                            }
                        }
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        FieldError error = new FieldError(target.getClass().getSimpleName(), descriptor.getName()
                              , "List 형태의 하위 정보에 대한 validation에 실패하였습니다");
                        errors.getFieldErrors().add(error);
                    }
                }
            }
        }
    }
}


Spring에서 간단히 DataBind에 등록만 해주면 된다.


@ControllerAdvice
public class WebDataBindHandler {

    @Inject
    private LocalValidatorFactoryBean localValidatorFactoryBean;

    @InitBinder
    void initBinder(WebDataBinder binder) {
        binder.addValidators(new JSR303CollectionListValidator(localValidatorFactoryBean));
    }
}