네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'programming language'에 해당되는 글 67건


반응형

Java에서 Enum을 활용적으로 !

 

Enum을 통해 얻는 기본적인 장점들은 아래와 같다.

1. 문자열과 비교해, IDE의 적극적인 지원을 받을 수 있다.

- 자동완성, 오타검증, 텍스트 리팩토링

2. 허용 가능한 값들을 제한할 수 있다.

3. 리팩토링 시, 변경 범위가 최소화된다.

- 내용의 추가가 필요하더라도, Enum 코드 외에 수정할 필요가 없다.

 

이 장점들은 모든 언어들의 Enum에서 얻을 수 있는 공통적인 장점이다.

 

하지만, Java의 Enum은 이보다 더 많은 장점을 가지고 있다.

Java의 Enum은 완전한 기능을 가진 클래스이기 때문이다. (C/C++의 경우 Enum이 결국 int 값이다.)

 

1. 데이터들간의 연관관계 표현

origin 테이블에 있는 내용을 2개의 테이블 (테이블명: table1, table2)에 등록하는 기능이 있다고 하자.

origin 테이블의 값은 "Y", "N"으로 저장되는데 table1, table2는 각각 "1", "0" / true, false 형태로 저장된다고 한다.

그럼 이를 분류하는 메서드를 다음과 같이 만들 것이다.

public class LegacyCase {
    
    public String toTable1Value(String originValue) {
        if ("Y".equals(originValue)) {
            return "1";
        } else {
            return "0";
        }
    }
    
    public boolean toTable2Value(String originValue) {
        if ("Y".equals(originValue)) {
            return true;
        } else {
            return false;
        }
    }
}

기능상의 문제는 없지만, 문제가 있다.

1. "Y", "1", true가 모두 같은 의미라는 것을 알 수 없다.

"Y"란 값이 "1"이 될 수도 있고 true가 될 수도 있다는 것을 확인하려면 항상 위에서 선언된 클래스와 메서드를 찾아야 한다.

2. 불필요한 코드량이 많다.

- "Y", "N"이외에 "R", "S" 등의 추가 값이 필요한 경우, if문을 포함하여 메서드 단위로 코드가 증가하게 된다.

- 동일한 타입의 값이 추가되는 것에 비해 너무 많은 반복성 코드가 발생하게 된다.

 

그래서 이 부분을 Enum으로 추출해보자.

public enum TableStatus {
    
    Y("1", true);
    N("0", false);
    
    private String table1Value;
    private boolean table2Value;
    
    TableStatus(String table1Value, boolean table2Value) {
        this.table1Value = table1Value;
        this.table2Value = table2Value;
    }
    
    public String getTable1Value() {
        return table1Value;
    }
    
    public boolean getTable2Value() {
        return table2Value;
    }
}

"Y", "1", true 가 한 묶음으로, "N", "0", false가 한 묶음이 된 것을 코드로 확인할 수 있다.

또한 추가 타입이 필요한 경우에도, Enum 상수와 get 메서만 추가하면 된다.

(lombok의 @Getter 를 사용하여 Enum의 get 메서드를 대신할 수 있다.)

 

@Test
public void test() throws Exception {
    
    // given
    TableStatus origin = selectFromOriginTable();
    
    String table1Value = origin.getTable1Value();
    String table2Value = origin.getTable2Value();
    
    assertThat(origin, is(TableStatus.Y));
    assertThat(table1Value, is("1"));
    assertThat(table2Value, is(true));
}

 

2. 상태와 행위를 한 곳에서 관리

서로 다른 계산식을 적용해야할 때가 있다.

DB에 저장된 code의 값이 "CALC_A"일 경우엔 값 그대로, "CALC_B"일 경우에 10을 곱해서, "CALC_C"인 경우에는 3을 곱해서 전달해야 한다.

가장 쉬운 해결방법은 아래와 같이 static 메서드를 작성해서 필요한 곳에 호출하는 방식이다.

public class LegacyCalculator {
    
    public static long calculate(String code, long originValue) {
        if ("CALC_A".equals(code)) {
            return originValue;
        } else if ("CALC_B".equals(code)) {
            return originValue * 10;
        } else if ("CALC_B".equals(code)) {
            return originValue * 3;
        } else {
            return 0;
        }
    }
}

이렇게 메서드를 분리하고 사용하다 보면, 코드는 코드대로 조회하고, 계산은 별도의 클래스와 메서드에서 진행하게 된다.

@Test
public void 코드에_따른_서로다른_계산_legacy() throws Exception {
    String code = selectCode();
    long originValue = 10000L;
    long result = LegacyCalculator.calculator(code, originValue);
    
    assertThat(result, is(10000L));
}

이 상황에 문제가 있다.

LegacyCalculator의 메서드와 code가 서로 관계가 있음을 표현할 수 없기 때문이다.

이런 경우,

1. 똑같은 기능을 하는 메서드를 중복 생성할 수 있다.

- 히스토리 관리가 되지 않은 상태에서 계산 메서드를 중복 생성할 수 있다.

- 관리 포인트가 증가할 확률이 매우 높다.

2. 계산 메서드를 누락할 수 있다.

- 결국 문자열과 메서드로 분리되어 있어, 이 계산 메서드를 써야함을 알 수 없어 새로운 기능 생성 시 계산 메서드 호출이 누락될 수 있다.

 

"DB에서 뽑은 특정 값은 지정된 메서드와 관계가 있다."

역할과 책임이라는 관점에서 봤을 때, 위 메시지는 Code에 책임이 있다.

public class CalculatorType {

    CALC_A(value -> value),
    CALC_B(value -> value * 10),
    CALC_C(value -> value * 3),
    CALC_ETC(value -> 0L);
    
    private Function<Long, Long> expression;
    
    CalculatorType(Function<Long, Long> expression) {
        this.expression = expression;
    }
    
    public long calculate(long value) {
        return expression.apply(value);
    }
}

Code가 본인 만의 계산식을 가지도록 지정하였다. (Java8이 업데이트되면서, 인자값으로 함수를 사용할 수 있게 되었다.)

 

Entity 클래스에서 선언할 경우에 String이 아니라 enum을 선언하면 된다.

@Column
@Enumerated(EnumType.STRING)
private CalculatorType calculatorType;

실제로 사용하는 곳에서도 직접 Code에게 계산을 요청하자.

@Test
public void 코드에_따라_서로다른_계산_enum() throws Exception {
    CalculatorType code = selectCode();
    long originValue = 10000L;
    long result = code.calculate(originValue);
    
    assertThat(result, is(10000L));
}

3. 데이터 그룹관리

결제라는 데이터는 '결제 종류'와 '결제 수단'이라는 2가지 형태로 표현된다.

예를 들어 신용카드 결제는 '신용카드'라는 결제 수단과 '카드'라는 결제 종류가 있다.

 

결제된 건이 어떤 결제 수단으로 진행되며, 결제 방식이 어떤 결제 종류에 속하는지 확인해야한다고 하자.

이를 해결하는 가장 쉬운 방법은 if문이다.

public class LegacyPayGroup {
    
    public static String getPayGroup(String payCode) {
        
        if ("ACCOUNT_TRANSFER".equals(payCode) || "PERMITTANCE".equals(payCode) || "ON_SITE_PAYMENT".equals(payCode) || "TOSS".equals(payCode)) {
            return "CASH";
        } else if ("PAYCO".equals(payCode) || "CARD".equals(payCode) || "KAKAO_PAY".equals(payCode) || "BAEMIN_PAY".equals(payCode)) {
            return "CARD";
        } else if ("POINT".equals(payCode) || "COUPON".equals(payCode)) {
            return "ETC";
        } else {
            return "EMPTY";
        }
    }
}

여기서도 여러 문제가 있다.

1. 둘의 관계를 파악하기 힘들다.

- 위 메서드는 포함관계를 나타내는 것일까. 아니면 단순한 대체값을 리턴한 것일까?

- 현재는 결제 종류가 결제 수단을 포함하고 있는 관계인데, 메서드만으로 표현이 불가능하다.

2. 입력값과 결과값이 예측 불가능하다.

- 결제 수단의 범위를 지정할 수 있어서 문자열이면 전부 파라미터로 전달될 수 있다.

- 마찬가지로 결과를 받는 쪽에서도 문자열을 받기 때문에 결제 종류로 지정된 값만 받을 수 있도록 검증코드가 필요하게 된다.

3. 그룹별 기능을 추가하기가 어렵다.

- 또 다시 결제 종류에 따른 if문으로 메서드를 만들어야 하나?

 

각각의 메서드는 원하는 때에 사용하기 위해 독립적으로 구성할 수 밖에 없는데

그럴 때마다 결제 종류를 분기하는 코드가 필수적으로 필요하다. 

 

각 타입은 본인이 수행해야할 기능과 책임과 가질 수 있게 하기 위해 enum 타입으로 변경하자.

@Getter
public enum PayGroup {

    CASH("현금", Arrays.asList("ACCOUNT_TRANSFER", "REMITTANCE", "ON_SITE_PAYMENT", "TOSS")),
    CARD("카드", Arrays.asList("PAYCO", "CARD", "KAKAO_PAY", "BAEMIN_PAY")),
    ETC("기타", Arrays.asList("POINT", "COUPON")),
    EMPTY("없음", Collections.EMPTY_LIST);
    
    private String title;
    private List<String> payList;
    
    PayGroup(String title, List<String> payList) {
        this.title = title;
        this.payList = payList;
    }
    
    public static PayGroup findByPayCode(String code) {
        return Arrays.stream(PayGroup.values())
                .filter(payGroup -> payGroup.hasPayCode(code))
                .findAny()
                .orElse(EMPTY);
    }
    
    public boolean hasPayCode(String code) {
        return payList.stream()
                .anyMatch(pay -> pay.equals(code));
    }
}

Java의 Enum은 결국 클래스이기 때문에 Enum 상수에 결제 종류 문자열 리스트를 가지도록 했다.

@Test
public void PayGroup에게_직접_결제종류_물어보기_문자열() throws Exception {
    String payCode = selectPayCode();
    PayGroup payGroup = PayGroup.findByPayCode(payCode);
    
    assertThat(payGroup.name(), is("BAEMIN_PAY"));
    assertThat(payGroup.getTitle(), is("배민페이"));
}

하지만, 해결되지 않은 것이 있다.

결제수단은 문자열로 되어있다.

DB 테이블의 결제수단 컬럼에 잘못된 값을 등록하거나, 파라미터로 전달된 값이 잘못되었을 경우가 있을 때 관리가 되지않을 위험이 있다.

public enum PayType {

    ACCOUNT_TRANSFER("계좌이체"),
    REMITTANCE("무통장입금"),
    ON_SITE_PAYMENT("현장결제"),
    TOSS("토스"),
    PAYCO("페이코"),
    CARD("신용카드"),
    KAKAO_PAY("카카오페이"),
    BAEMIN_PAY("배민페이"),
    POINT("포인트"),
    COUPON("쿠폰"),
    EMPTY("없음");
    
    private String title;
    
    PayType(String title) {
        this.title = title;
    }
    
    public String getTitle() {
        return title;
    }
}

이렇게 Enum으로 결제종류를 만들고 PayGroup에서 사용해보자.

@Getter
public enum PayGroupAdvanced {

    CASH("현금", Arrays.asList(PayType.ACCOUNT_TRANSFER, PayType.REMITTANCE, PayType.ON_SITE_PAYMENT, PayType.TOSS)),
    CARD("카드", Arrays.asList(PayType.PAYCO, PayType.CARD, PayType.KAKAO_PAY, PayType.BAEMIN_PAY)),
    ETC("기타", Arrays.asList(PayType.POINT, PayType.COUPON)),
    EMPTY("없음", Collections.EMPTY_LIST);
    
    private String title;
    private List<String> payList;
    
    PayGroup(String title, List<String> payList) {
        this.title = title;
        this.payList = payList;
    }
    
    public static PayGroup findByPayType(PayType payType) {
        return Arrays.stream(PayGroupAdvanced.values())
                .filter(payGroup -> payGroup.hasPayCode(payType))
                .findAny()
                .orElse(EMPTY);
    }
    
    public boolean hasPayCode(PayType payType) {
        return payList.stream()
                .anyMatch(pay -> pay == payType));
    }
}
@Test
public void PayGroup에게_직접_결제종류_물어보기_enum() throws Exception {
    PayType payType = selectPayType();
    PayGroupAdvanced payGroup = PayGroupAdvanced.findByPayType(payType);
    
    assertThat(payGroup.name(), is("BAEMIN_PAY"));
    assertThat(payGroup.getTitle(), is("배민페이"));
}

PayType으로 데이터를 받아 타입 안전성까지 확보하여 관련 처리를 진행할 수 있다.

4. 관리 주체를 DB에서 객체로

DB에서 코드를 관리할 경우 문제가 발생할 수 있다.

1. 코드명만 봐서는 무엇을 나타내는지 알 수 없다.

- 문서화가 되어있다 하더라도, 문서 업데이트가 잘 되어있는지 확신할 수 없어 DB를 다시 찾아봐야 하는 번거로움이 있다.

2. 항상 코드 테이블 조회 쿼리가 실행되어야 했다.

- 화면에 표시하기 위해 코드 테이블을 조회해야 한다.

 

Enum을 바로 JSON으로 리턴하게 되면, 상수 name만 출력된다.

Enum의 name과 title 모두 필요한 경우가 많다.

클래스의 생성자로 일관된 타입을 받기 위해 인터페이스를 하나 생성하자.

public interface EnumMapperType {
    String getCode();
    String getTitle();
}
@Getter
@ToString
public class EnumMapperValue {
    
    private String code;
    private String title;
    
    public EnumMapperValue(EnumMapperTupe enumMapperType) {
        code = enumMapperType.getCode();
        title = enumMapperType.getTitle();
    }  
}

Enum은 미리 선언한 인터페이스를 구현하면 된다.

public enum FeeType implements EnumMapperType {
    
    PERCENT("정율"),
    MONEY("정액");
    
    private String title;
    
    FeeType(String title) {
        this.title = title;
    }
    
    @Override
    public String getCode() {
        return name();
    }
    
    @Override
    public String getTitle() {
        return title;
    }
}

 

이제 Enum을 Value 클래스로 변경한 후, 전달하자.

@GetMapping("/no-bean-categories")
public List<EnumMapperValue> getNoBeanCategory() {
    return Arrays.stream(FeeType.values())
            .map(EnumMapperValue::new)
            .collect(Collectors.toList());
}

/*
[
  {
    "code": "PERCENT",
    "title": "정율"
  },
  {
    "code": "MONEY",
    "title": "정액"
  }
]

*/

 

 

 

 출처 

https://techblog.woowahan.com/2527/

 

Java Enum 활용기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 E

techblog.woowahan.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

JSON.parse()와 JSON.stringify()

- 자바스크립트에서 JSON 내장 객체를 이용하여 JSON 형식으로 표현된 데이터를 다루는 방법

JSON이란?

JSON은 Javascript Object Notation의 약자로, 데이터를 문자열의 형태로 나타내기 위해서 사용된다. 이름이 보여주듯 JSON은 본래 자바스크립트에서 파생되었지만 현재는 거의 표준으로 자리잡아 대부분의 언어에서 지원하는 데이터포맷이다. JSON은 특히 네트워크를 통해 서로 다른 시스템들이 데이터를 주고 받을 때 많이 사용되기 때문에 어렵지 않게 접할 수 있다.

JSON으로는 객체, 배열, 숫자, 문자열, 불린(boolean), 널(null)과 같은 다양한 데이터를 나타낼 수 있다.

JSON 내장 객체

자바스크립트에서는 JSON 포맷의 데이터를 간편하게 다룰 수 있도록 JSON 이라는 객체를 내장하고 있다. 이 객체는 자바스크립트 코드를 브라우저에서 실행하든 Node.js 런타임에서 실행하든 상관없이 전역(global)에서 접근이 가능합니다.

JSON 내장 객체는 Javascript 객체와 JSON 문자열 간의 상호 변환을 수행해주는 두 개의 메서드를 제공한다.

- JSON.parse()

- JSON.stringify()

JSON.parse(): JSON 문자열을 Javascript 객체로 변환

JSON 문자열을 Javascript 객체로 변환할 때는 JSON 객체의 parse() 메서드를 사용한다. parse() 메서드는 JSON 문자열을 인자로 받고 결과값으로 javascript 객체를 반환한다.

const str = `{
  "name": "홍길동",
  "age": 25,
  "married": false,
  "family": { "father": "홍판서", "mother": "춘섬" },
  "hobbies": ["독서", "도술"],
  "jobs": null
}`;

JSON.parse() 메서드에 str을 인자로 넘겨 호출해 결과값을 obj라는 변수에 저장한다.

const obj = JSON.parse(str);

obj 에 저장된 값을 콘솔에 출력해보면 JSON 문자열 형태의 데이터가 Javascript 객체의 형태로 변환되어 출력되는 것을 확인하자.

console.log(obj);

{
    name: "홍길동",
    age: 25,
    married: false,
    family: {
        father: "홍판서",
        mother: "춘섬"
    },
    hobbies: [
        "독서",
        "도술"
    ],
    jobs: null
}

JSON 문자열에서는 key 를 나타낼 때 반드시 쌍따옴표로 감싸주어야 하지만, Javascript 객체에서는 쌍따옴표를 꼭 사용할 필요가 없다. 

Javascript로 변환된 데이터는 .이나 [] 기호를 활용하여 각 속성에 접근할 수 있다.

> obj.name
< '홍길동'
> obj.age
< 25
> obj.married
< false
> obj.family
< {father: '홍판서', mother: '춘섬'}
> obj.family.mother
< '춘섬'
> obj.hobbies
< ['독서', '도술']
> obj.hobbies[1]
< '도술'
> obj.jobs
< null

외부에서 문자열의 형태로 주어진 데이터를 해당 언어에서 다루기 용이하도록 내장 데이터 타입으로 변환하는 과정을 역직렬화(deserialization)이라고 한다. 클라이언트에서 JSON 포맷으로 데이터를 보내면 서버에서 우선 Javascript 객체로 변환 후, 데이터를 처리한다.

JSON.stringify(): Javascript 객체를 JSON 문자열로 변환

역으로 Javascript 객체를 JSON 문자열로 변환할 때는 JSON 객체의 stringify() 메서드를 사용한다. stringify() 메서드는 Javascript 객체를 인자로 받고 JSON 문자열을 반환한다.

const obj = {
  name: "홍길동",
  age: 25,
  married: false,
  family: {
    father: "홍판서",
    mother: "춘섬",
  },
  hobbies: ["독서", "도술"],
  jobs: null,
}; // javascript 객체

JSON.stringify() 메서드에 obj 를 인자로 넘겨 호출해보자. 

const str = JSON.stringify(obj);

console.log(str);
'{"name":"홍길동","age":25,"married":false,"family":{"father":"홍판서","mother":"춘섬"},"hobbies":["독서","도술"],"jobs":null}'

str에 저장된 값을 콘솔로 출력해보면 Javascript 객체의 형태인 데이터가 JSON 형식의 문자열로 변환되어 출력되는 것을 확인할 수 있다.

 

stringify() 메서드의 3번째 인자로 들여쓰기 할 공백의 크기도 지정해줄 수 있다.

const str2 = JSON.stringify(obj, null, 2);
console.log(str2);

{
  "name": "홍길동",
  "age": 25,
  "married": false,
  "family": {
    "father": "홍판서",
    "mother": "춘섬"
  },
  "hobbies": [
    "독서",
    "도술"
  ],
  "jobs": null
}

당연하게도, JSON 형식의 문자열로 변환된 데이터는 더이상 . 이나 [] 기호를 사용하여 속성에 접근할 수 없다.

> str.name
< undefined

특정 언어의 내장 타입의 데이터를 외부에 전송하기 용이하도록 문자열로 변환하는 과정을 직렬화(serialization)이라고 한다. 

 

 출처 

https://www.daleseo.com/js-json/

 

JSON.parse()와 JSON.stringify()

Engineering Blog by Dale Seo

www.daleseo.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

코드를 작성할 때 도메인에서 사용하는 용어는 매우 중요하다. 
도메인에서 사용하는 용어를 코드에 반영하지 않으면
그 코드는 개발자에게 코드의 의미를 해석해야 하는 부담을 준다.
도메인 모델의 Entity나 value에 공개 set 메서드만 넣지 않아도 일관성이 깨질 가능성이 줄어든다.
공개 set 메서드를 사용하지 않으면 의미가 드러나는 메서드를 사용해서 구현할 가능성이 높아진다.
예를 들어, set 형식의 이름을 갖는 공개 메서드를 사용하지 않으면
자연스럽게 cancel이나 changePassword 처럼 의미가 더 잘 드러나는 이름을 사용하는 빈도가 높아진다.
- 도메인 주도 개발 시작하기, 최범균-

 

'setter를 지양하라'라는 말을 구체적으로 서술하면

외부에서 필드의 값을 변경하기 위해 접근할 때,

단순하게 setXXX라는 이름의 메서드 하나만 있는 것이 아니라,

필드의 값을 변경하려는 목적을 파악하여

그 목적을 잘 표현하는 메서드를 제공하라.

 

 

예제

'회원' 객체는 회원 이름과 회원 상태 속성을 가지고 있다.

setState 메서드는 회원의 상태를 변경하는 '공개 set 메서드'이다.

public class 회원 {
    
    private String name;
    private String state;
    
    public String getName() {
        return name;
    }
    
    public String getState() {
        return state;
    }
    
    public void setState(String state) {
        this.state = state;
    }
}

회원관리 서비스는 회원의 상태를 조정하여 회원을 차단하거나, vip로 승격한다.

public class 회원관리 {
    
    public void blockMember(회원 회원) {
    
        if (회원.getName().equals("jyjeon")) { // 공통 도메인 규칙
            throw new IllegalArgumentException("닉네임이 jyjeon인 사람의 정보는 변경할 수 없습니다.");
        }
        if (회원.getState().equals("vip")) { // 회원 차단 도메인 규칙
            throw new IllegalArgumentException("vip는 차단할 수 없습니다.");
        }
        
        회원.setState("blocked");
    }
    
    public void upgradeMemberToVip(회원 회원) {
        if (회원.getName().equals("jyjeon")) { // 공통 도메인 규칙
            throw new IllegalArgumentException("닉네임이 jyjeon인 사람의 정보는 변경할 수 없습니다.");
        }
        if (회원.getState().equals("blocked")) { // vip 상향 도메인 규칙
            throw new IllegalArgumentException("차단된 사용자는 vip가 될 수 없습니다.");
        }
        
        회원.setState("vip");
    }
}

회원의 상태(state 필드 값)를 변경하기 위해서는 회원 도메인의 규칙을 지켜야 합니다.

- 특정 닉네임을 가진 회원의 상태는 변경할 수 없다.

- vip는 차단할 수 없다.

- 차단된 회원은 vip가 될 수 없다.

 

공개 set 메서드의 문제점 1) 도메인 로직의 분산

공개 set 메서드는 도메인의 의미나 의도를 표현하지 못하고
도메인 로직을 도메인 객체가 아닌 응용 영역이나 표현 영역으로 분산시킨다.
도메인 로직이 한 곳에 응집되지 않으므로 
코드를 유지보수할 때에도 분석하고 수정하는 데 더 많은 시간이 필요하다.
- 도메인 주도 개발 시작하기, 최범균 -

"도메인 로직이 응용 영역이나 표현 영역으로 분산된다."의 의미를 알아보자.

 

도메인 모델 패턴에서, 사용자의 요청을 처리하고 사용자에게 정보를 보여주는 표현 영역이라 한다.

MVC 패턴은 http 요청을 받고 응답하는 컨트롤러 클래스들은 표현 영역이라 할 수 있다.

사용자가 요청한 기능을 실행하는 영역을 응용 영역이라 한다. MVC 패턴은 비즈니스 로직을 수행하는 서비스 클래스가 응용 영역이다.

 

특정 도메인을 개념적으로 표현하고, 도메인의 핵심 규칙이 있는 영역을 도메인 영역이라고 한다. 도메인 영역은 도메인 객체 하나를 가리킬 수도 있지만, 연관이 있는 상위 도메인과 하위 도메인들을 한 다발로 묶어 Aggregate(애그리거트)라는 군집을 가리킬 수도 있다.

 

예제에서 '회원의 상태를 변경하기 위해서 회원이 특정한 이름을 가지고 있는지 확인해야 한다.'는 규칙이 있다. 도메인의 속성을 변경하기 위한 핵심 규칙이기 때문에 이 규칙은 도메인 영역에서 구현되어야 한다.

 

하지만, 예제에서는 도메인 규칙을 표현하는 코드가 비즈니스 로직을 수행하는 '회원관리' 클래스에 작성되어 있다. 다시말해, 도메인 로직이 응용 영역으로 분산되어 있다.

 

1. 서비스가 커지면서 회원의 상태를 변경하는 곳이 29억 군데라면? 29억 군데에 규칙을 표현해야 한다.

2. 회원의 상태를 변경하기 위한 규칙이 추가되거나 삭제된다면? 29억 군데의 비즈니스 로직을 전부 수정해야 한다.

 

공개 set 메서드를 사용할 때, 도메인 로직이 한 곳에 응집되지 않으므로 코드를 유지 보수할 때에도 분석하고 수정하는 데 시간이 필요하다. 

 

 

단순히 값을 변경하는 공개 set 메서드가 일으킬 문제에 대해 알아보았다.

그럼 값을 변경하는 것에 더해서 공통 규칙을 set 메서드에 넣어주면 해결이 될까?

// 회원.java
public void setState(String state) {
    if (this.name.equals("jyjeon")) {
        throw new IllegalArgumentException("닉네임이 jyjeon인 회원의 정보는 변경할 수 없습니다.");
    }
    
    this.state = state;
}
public class 회원관리 {
    
    public void blockMember(회원 회원) {
        if (회원.getState().equals("vip")) {
            throw new IllegalArgumentException("vip는 차단할 수 없습니다.");
        }
        
        회원.setState("blocked"); // 공통 규칙은 set 메서드에 있다.
    }

    public void upgradeMembetToVip(회원 회원) {
        if (회원.getState().equals("blocked")) {
            throw new IllegalArgumentException("차단된 사용자는 vip가 될 수 없습니다.");
        }
        
        회원.setState("vip"); // 공통 규칙은 set 메서드에 있다.
    }
}

 

하지만, 'vip는 차단할 수 없다.' '차단된 회원은 vip가 될 수 없다.' 라는 규칙도 도메인 규칙이고, 도메인 영역에 있어야 한다. 지금은, 그 책임을 응용 영역에서 해결하고 있다.

 

그렇다면, 특정 조건에 따른 도메인 규칙도 set 메서드를 넣으면 해결이 될까?

public void setState(String state) {
    if (this.name.equals("jyjeon")) {
        throw new IllegalArgumentException("닉네임이 jyjeon인 회원의 정보는 변경할 수 없습니다.");
    }

    if (state.equals("blocked")) {
        if (this.state.equals("vip")) {
            throw new IllegalArgumentException("vip는 차단할 수 없습니다.");
        }
    } // 회원을 차단할 때의 도메인 규칙?

    if (state.equals("vip")) {
        if (this.state.equals("blocked")) {
            throw new IllegalArgumentException("차단된 사용자는 vip가 될 수 없습니다.");
        }
    } // vip로 상향할 때의 도메인 규칙?

    this.state = state;
}
public class 회원관리 {
    public void blockMember(회원 회원) {
        회원.setState("blocked");
    }

    public void upgradeMemberToVip(회원 회원) {
        회원.setState("vip");
    }
}

이런 코드로 해결하는 것은 좋지 않다. 하나의 공간에 너무 많은 책임이 있다.

 

문제점 1 (도메인 로직의 분산)의 해결책

필드의 값을 변경하려는 목적을 제대로 파악하여
그 목적을 잘 표현하는 메서드를 제공해라.
public class 회원 {
    private String name;
    private String state;

    public String getName() {
        return name;
    }
    
    public String getState() {
        return state;
    }

    public void blockMember() {
        verifyName();
        if (this.state.equals("vip")) {
            throw new IllegalArgumentException("vip는 차단할 수 없습니다.");
        }

        this.state = "blocked";
    }

    public void upgradeMemberToVip() {
        verifyName();
        if (this.state.equals("blocked")) {
            throw new IllegalArgumentException("차단된 사용자는 vip가 될 수 없습니다.");
        }

        this.state = "vip";
    }

    private void verifyName() { // 공통 도메인 규칙
        if (this.name.equals("jyjeon")) {
            throw new IllegalArgumentException("닉네임이 jyjeon인 회원의 정보는 변경할 수 없습니다.");
        }
    }
}
public class 회원관리 {
    public void blockMember(회원 회원) {
        회원.blockMember(); // 도메인 관련 로직을 도메인 영역에 위임
    }
    
    public void upgradeMemberToVip(회원 회원) {
        회원.upgrageMemberToVip(); // 도메인 관련 로직을 도메인 영역에 위임
    }
}

이 코드가 기존의 문제점을 어떻게 해결해줄까?

1. 공통 도메인 규칙이 응용 영역으로 분산된다.

회원 도메인의 상태를 변경할 때 공통으로 필요한 도메인 규칙을 도메인 영역 안에서 구현하고 지키게함으로써, 도메인 영역 외의 영역들에서는 더이상 공통 도메인 규칙에 대해 신경 쓸 필요가 없다.

헌데, 도메인 영역 안의 메서드라 해도, 이런 식의 코드는 공통 도메인 규칙을 검사하는 것을 깜빡할 수도 있다.

public class 회원 {
    private String name;
    private String state;

    public String getName() {
        return name;
    }

    public void blockMember() {
        if (this.state.equals("vip")) {
            throw new IllegalArgumentException("vip는 차단할 수 없습니다.");
        }

        setState("blocked");
    }

    public void upgradeMemberToVip() {
        if (this.state.equals("blocked")) {
            throw new IllegalArgumentException("차단된 사용자는 vip가 될 수 없습니다.");
        }

        setState("vip");
    }

    private void setState(String state) {
        verifyName(); // 공통 도메인 규칙과 필드에 값을 할당하는 로직을 묶어서 메서드로 뺀다.
        this.state = state;
    } 

    private void verifyName() {
        if (this.name.equals("jyjeon")) {
            throw new IllegalArgumentException("닉네임이 jyjeon인 회원의 정보는 변경할 수 없습니다.");
        }
    }

    public String getState() {
        return state;
    }
}

공통 도메인 규칙과 필드에 값을 할당하는 코드를 setState()라는 메서드로 묶어 추출했다. 필드에 값을 할당하고자 한다면 직접 값을 할당하지 말고 내부적으로 다시 setState()를 호출하도록 만들면, 값 할당과 함께 공통 도메인 규칙을 검사하는 로직이 실행된다.

 

2. 상황별 도메인 규칙이 응용 영역으로 분산된다.

각 상황별로 각각의 공개 메서드를 만들어서 그 안에 도메인 규칙을 구현하고, 그 각각의 메서드를 외부에 제공한다.

응용 영역에서 도메인 영역과 소통할 때, 도메인 모델과 관련된 모든 규칙들은 도메인 모델 안에서 알아서 구현하도록 했기 때문에, 응용 영역은 어떠한 도메인 규칙에 대해서도 신경쓸 필요가 없다. (캡슐화)

 

도메인과 관련된 어떤 처리가 필요할 때는 도메인 영역에서 제공하는 공개 메서드들 중에 목적에 맞는 메서드를 호출하고, 응용영역은 비즈니스 로직에만 집중할 수 있습니다.

 

비즈니스 로직에서 도메인 규칙을 가지고 있음 -> 응용 영역으로 도메인 영역의 책임이 분산된다. -> 응용 영역에 너무 많은 책임을 가지게 된다.

공개 set 메서드의 문제점 2) 잘못 정의한 메시지

Never Trust User (사용자를 믿지 마라)

'객체 지향의 사실과 오해 (조영호)'라는 책에서 객체와 객체 간에 주고 받는 메시지의 역할을 강조한다.

메시지를 수신받은 객체는 우선 자신이 해당하는 메시지를 처리할 수 있는지 확인한다.
메시지를 처리할 수 있다는 이야기는 객체가 해당 메시지에 해당하는 행동을 수행해야 할 책임이 있다는 것을 의미한다.
따라서 근본적으로 메시지의 개념은 책임의 개념과 연결된다.
송신자는 메시지 전송을 통해서만 다른 객체의 책임을 요청할 수 있고, 수신자는 오직 메시지 수신을 통해서만 자신의 책임을 수행할 수 있다.
따라서 객체가 수신할 수 있는 메시지의 모양이 객체가 수행할 책임의 모양을 결정한다.
- 객체지향의 사실과 오해, 조영호 -

객체가 객체에게 어떤 행동을 지시하는 유일한 방법은 메시지를 주고받는 것이다.

set메서드를 가지고 있는데 메시지 상으로 'XXX를 바꾼다'라는 메시지를 받아 수행할 수 있다.

하지만, 실제로는 그 메서드를 실행했을 때 문제가 생길 수 있음에도 실행 권한을 주어 버렸으므로 문제가 발생하는 것이다.

 

문제점 2 (잘못 정의한 메시지) 해결책

도메인 모델의 entity나 value에 공개 set 메서드만 넣지 않아도 일관성이 깨질 가능성이 줄어든다.
공개 set 메서드를 사용하지 않으면 의미가 드러나는 메서드를 사용해서 구현할 가능성이 높아진다.
예를 들어, set 형식의 이름을 갖는 공개 메서드를 사용하지 않으면
자연스럽게 cancel이나 changePassword처럼 의미가 더 잘 드러나는 이름을 사용할 빈도가 높아진다.
public class Post {

    private Long id;
    private String title;
    private String content;
    private Member writer;
}

1. id는 유일한 식별자이다. 통상적으로 식별자 값은 데이터의 무결성과 연관관계 등 고려해야 할 제약조건이 많아 수정하지 않는 것을 권장한다.

2. title을 바꾸기 위해서는 새 title이 '영어 대/소문자, 숫자, 특수문자를 포함하여 10자 이상'을 만족하는지 검사한다.

검사를 마친 후, title를 바꾸도록 메서드를 구현하고 setTitle() 보단 changeTitle()로 실행의도를 분명히 들어낸다.

3. writer의 값을 바꾸는 것은 금지되어 있다. 관련 메서드는 제공하지 않는다.

 

public class Post {
    
    private Long id;
    private String title;
    private String content;
    private Member writer;
    
    public changeTitle(String newTitle) {
        checkTtile(newTitle);
        this.title = newTitle;
    }
    
    private void checkTitle(String title) {
        Pattern titlePattern = Pattern.compile("^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[$@$!%*#?&])[A-Za-z[0-9]$@$!%*#?&]{10,}$");
        Matcher matcher = titlePattern.matcher(title);

        if (!matcher.find()) {
            throw new IllegalArgumentException("title의 작성 양식에 맞지 않습니다.");
        }
    }
}

 

 출처  setter 쓰지 말라고만 하고 가버리면 어떡해요 - 여우

https://velog.io/@backfox/setter-%EC%93%B0%EC%A7%80-%EB%A7%90%EB%9D%BC%EA%B3%A0%EB%A7%8C-%ED%95%98%EA%B3%A0-%EA%B0%80%EB%B2%84%EB%A6%AC%EB%A9%B4-%EC%96%B4%EB%96%A1%ED%95%B4%EC%9A%94

 

setter 쓰지 말라고만 하고 가버리면 어떡해요

부트캠프 과정을 잘 견뎌내고 팀프로젝트에서 백엔드를 맡은 엄준식(27)씨.백엔드 커리큘럼을 유난히 즐거워했던 준식씨였기에 자신이 맡은 파트가 꽤 마음에 드는 모양이다.준식씨: 됐다! 게시

velog.io

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

https://frogand.tistory.com/190

 

지난 포스팅에 @CurrentUser란 커스텀 어노테이션을 만들면서 @Target, @Retention을 사용했습니다.

 

meta-annotation

메타 어노테이션은 다른 어노테이션에서도 사용되는 어노테이션을 말하며, 커스텀 어노테이션을 생성할 때 주로 생성된다.

 

@Target

@Target은 자바 컴파일러가 어노테이션이 어디에 적용될지 결정하기 위해 사용한다.

예를들어 Spring의 @Service 어노테이션의 ElementType.Type은 해당 어노테이션은 타입 선언 시 사용한다는 의미다.

 

ElementType.PACKAGE : 패키지 선언

ElementType.TYPE : 타입 선언

ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언

ElementType.CONSTRUCTOR : 생성자 선언

ElementType.FIELD : 멤버 변수 선언

ElementType.LOCAL_VARIABLE : 지역 변수 선언

ElementType.METHOD : 메서드 선언

ElementType.PARAMETER : 전달 인자 선언

ElementType.TYPE_PARAMETER : 전달인자 타입 선언

ElementType.TYPE_USE : 타입 선언

 

@Retention

@Retention은 어노테이션이 실제로 적용되고 유지되는 범위를 의미한다.

 

RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM에 의해 계속 참조가 가능하다. 주로 리플렉션이나 로깅에 많이 사용한다.

RetentionPolicy.CLASS : 컴파일러가 클래스를 참조할 때까지 유효하다.

RetentionPolicy.SOURCE : 컴파일 전까지만 유효한다. 즉, 컴파일 이후에는 사라지게 된다.

 

 

 출처 

https://sanghye.tistory.com/39

 

[Spring] Meta Annotation 이란?(@Target, @Retention)

Spring 에서는 Anntotation 사용에 대한 기능을 상당히 많이 제공하고 있습니다. 주로 사용하는 @Controller, @Service, @Repostiroy 등 많은 Annotation 이 존재합니다. 해당 Annotion 은 각 기능에 필요한 만큼..

sanghye.tistory.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형
kill -9로 종료하는 것은 좋지않다. jvm shutdown hook 또는 spring의 @PreDestroy의 실행을 보장하기 힘들다. kill -2(SIGINT) 혹은 kill -15(SIGTERM)을 쓰는 것이 좋다.

kill 명령어와 Graceful Shutdown에 대해서 알아보고자 한다.

 

 kill 명령어 알아보기 

https://frogand.tistory.com/69

 

[Linux] ps 명령어 / kill 명령어 / 프로세스 확인 및 죽이기 / 프로세스 일괄 종료 / kill process

# 프로세스란? 실행중인 프로그램 # ps 명령어 - Process 와 관련 linux 명령어는 ps 입니다. 옵션 기능 -e 시스템 상의 모든 프로세스 정보를 출력 -f 상세한 정보를 출력 (full-format) $ ps -ef UID PID PPID C..

frogand.tistory.com

 

애플리케이션 구동도 중요하지만, 안전한 종료야 말로 신규 버전을 배포하기 위해 필수적으로 진행해야 한다. 

 

Graceful Shutdown이란?

프로그램이 종료될 때, 최대한 side effect없이 로직을 잘 처리하고 종료하는 것을 의미한다.

지속가능한 소프트웨어를 위해 폐기 가능한(Disposability) 시스템을 구성해야 한다. 그리고 소프트웨어의 안전성을 높이기 위하여 graceful shutdow이 필요하다. 프로세스가 갑작스러운 하드웨어 문제에 의해 죽는 상황이 발생하더라도 문제가 없는 견고한 프로그램을 만들어야 한다. 프로그래머는 이를 준비해야 한다. (The Twelve-Factor App 방법론)

 

SIGTERM, SIGKILL 차이점

kill -9 $PID는 강제종료이다. -9인 SIGKILL은 리소스를 정리하는 핸들러를 지정하지 않고 프로세스를 바로 죽인다는 의미이다. 만약, 실행 중인 쓰레드가 있더라도 이를 무시하고 중단하는데 혹시라도 굉장히 중요한 작업중이라면 최악의 상황이 일어날 수 있기 때문이다.

 

 

Spring boot에서는 actuator를 사용해서 graceful shutdown을 할 수 있다.

 

 참고자료 

https://blog.marcosbarbero.com/graceful-shutdown-spring-boot-apps/

 

Graceful Shutdown Spring Boot Applications

This guide walks through the process of graceful shutdown a Spring Boot application. The implementation of this blog post is originally created by Andy Wilkinson and adapted by me to Spring Boot 2. The code is based on this GitHub comment. Introduction A l

blog.marcosbarbero.com

https://heowc.dev/2018/12/27/spring-boot-graceful-shutdown/

 

Spring Boot - 안전하게 종료시키기 | 허원철의 개발 블로그

Spring Boot를 안전하게 종료시키는 방법에 대한 소개이다.

heowc.dev

https://kapentaz.github.io/spring/Spring-Boot-Actuator-Graceful-Shutdown/#

 

Spring Boot Actuator Graceful Shutdown

Spring Boot 환경에서 application을 shutdown 하는 방법 중 대표적인 것이 actuator의 shutdown endpoint 기능을 이용하는 것입니다. 이 endpoint는 예상과 달리 처리 중인 요청이 있더라도 그냥 shutdown 처리를 합니

kapentaz.github.io

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !