오늘 스터디 했던 내용 중 PDF 보충이 되는 내용에 대해 포스팅하고자 합니다.



1. FP 의 참조투명성을 이용한 다중 스레드 경쟁상태 제거.


참조투명성(같은 입력을 한다면, 언제나 같은 결과를 출력한다) 라는 원칙에 따라 스레드 경쟁상태를 피하는 법에 대해 살짝 맛보기를 하였습니다.


오늘 스터디에서는 경쟁상태를 피하고자 은행업무 예제를 한번 살펴보았습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
 * 계좌에 대한 클래스
 */
class Account{
    
    private Integer money = 0;
 
    /**
     * 500 원 입금을 하는 메소드
     */
    public void deposit_500() {
 
        money += 500;
 
    }
 
    /**
     * 돈을 전부 출력하는 메소드
     * 
     * <pre>
     *     돈을 전부 출력한다. (돈의 금액이 음수가 되면 예외를 발생)
     * </pre>
     */
    public void withDrawAll() {        
        int withDraw = money;
 
        if (money - withDraw >= 0) {
            money -= withDraw;
        }
 
        if (money < 0) {
           throw new RuntimeException("[에러발생] 돈의 금액이 음수가 되었음!! --> 돈의 현재 금액 : " + money);
        }
    }
}
 
cs


돈의 입금은 500원씩하며, 돈의 출금은 모두 합니다. (출금 시, 돈의 금액이 음수가 되면 예외가 발생합니다.)


스레드 간의 경재상태를 만들기 위해 아래와 같은 테스트코드를 돌려보겠습니다. 


앞써, 스레드란 프로세스에서 실행흐름의 단위를 말합니다. 


아래와 같은 경우 현실세계에 대입해보면, 동시에 한명은 500원씩 입금하고 두명은 금액 모두를 출금하는 형태라고 볼 수 있습니다. ㅡㅡ^


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Account account = new Account();
 
int count = 100000;
 
new Thread(() -> {
    // 100000 회 금액 모두를 출금하는 흐름1
    for (int i = 0; i < count; ++i) {
        account.withDrawAll();
    }
}).start();
 
new Thread(() -> {
    // 100000 회 금액 모두를 출금하는 흐름2
    for (int i = 0; i < count; ++i) {
        account.withDrawAll();
    }
}).start();
 
new Thread(() -> {
    // 100000 회 금액 500원을 입금하는 흐름3
    for (int i = 0; i < count; ++i) {
        account.deposit_500();
    }
}).start();
 
// CONSOLE LOG
java.lang.RuntimeException: [에러발생] 돈의 금액이 음수가 되었음!! --> 돈의 현재 금액 : 223000
java.lang.RuntimeException: [에러발생] 돈의 금액이 음수가 되었음!! --> 돈의 현재 금액 : 222500
cs


안타깝게도 동시성으로 각 행위를 실행했을 때, 금액이 음수가 되는 경우가 있나봅니다. 


심지어 에러로그 역시도 양수인 금액입니다. 

에러가 발생했지만 그 사이에 돈을 또 채워 넣었나 봅니다. (이러면, 디버그가 정말 어려워집니다.)


하지만 불굴의 프로그래머는 문제가 되는 지역이 withDrawAll 이라는 메소드라는 것을 알았으며, 운영체제적 지식도 있기 때문에 뮤텍스나 세마포어 등을 이용하여 동시적인 실행흐름이라도 이 곳은 꼭 한흐름만 실행되도록 을 걸었습니다. 


JAVA 개발자라면 간단한 키워드 synchronized 를 이용하여 이 문제를 해결했습니다.


아래와 같이 말이죠.  


1
2
3
4
5
6
7
8
9
10
11
public synchronized void withDrawAll() {
    int withDraw = money;
 
    if (money - withDraw >= 0) {
        money -= withDraw;
    }
 
    if (money < 0) {
        throw new RuntimeException("[에러발생] 돈의 금액이 음수가 되었음!! --> 돈의 현재 금액 : " + money);
    }
}
cs


하지만 개발자는 성능을 높이고자 병렬처리(흐름을 여러개 두어 일을 나눠처리)를 통해 효율을 높이고 싶었지만, 락이 걸려 순차처리(흐름을 한개두어 처리하는 방식, 특별히 병렬처리를 하지 않으면 순차처리방식) 를 하는만도 못하게 되었습니다.


이를 조금 더 효율적으로 하는 방법은 많겠지만, 우리는 FP 로 이 문제를 해결해보고자 합니다. 

FP 의 개념 중 하나인 순수함수로 이 문제를 해결할 수 있습니다.


1
2
3
4
5
6
7
8
9
public void withDrawAll() {
    int withDraw = money;
 
    money = Optional.of(money).filter(number -> number - withDraw >= 0).map(number -> number - withDraw).orElse(money);
 
    if (money < 0) {
        throw new RuntimeException("[에러발생] 돈의 금액이 음수가 되었음!! --> 돈의 현재 금액 : " + money);
    }
}
cs


물론 이 메소드 역시 에러가 발생하지 않습니다. 

파이프라인으로 작성된 부분은 같은 input 같은 result 를 보장합니다. (참조 투명성.)

(함수형 문법은 곧 배울테니, 생략합시다. 이정도가 있다 정도만 알고 가는 것으로.. ㅎㅎ)




2. 어댑터 패턴을 이용한 객체 변환


백-엔드 개발자 입장에서는 VO 를 쉽게 던지고 싶습니다. 


따로 다른 객체로 변형해야하는 과정은 매우 귀찮으며, 코드의 질을 떨어트립니다.


하지만 프론트-엔드 개발자의 경우 알아보기 쉬운 결과를 원하며, 이를 맞춰주기 위해서는 결국 다른 객체로 변형을 해야합니다.


결국 우리는 이런 로직을 모델에 해당하는 클래스에 정의해야합니다.


예제를 살펴봅시다.


우리는 아래의 HrSampleVo 를 사용한다고 가정합시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class HrSampleVo {
    private Integer memberSubjectSn;        // 프론트에서 원하는 정보1
    private String name;                    // 프론트에서 원하는 정보2
    private String sampleResult;
    private Boolean maleYn;
 
    public Integer getMemberSubjectSn() {
        return memberSubjectSn;
    }
 
    public void setMemberSubjectSn(Integer memberSubjectSn) {
        this.memberSubjectSn = memberSubjectSn;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getSampleResult() {
        return sampleResult;
    }
 
    public void setSampleResult(String sampleResult) {
        this.sampleResult = sampleResult;
    }
 
    public Boolean getMaleYn() {
        return maleYn;
    }
 
    public void setMaleYn(Boolean maleYn) {
        this.maleYn = maleYn;
    }
}
 
cs


프론트-엔드에서 원하는 정보는 구성원주체순번과 이름입니다. 즉 해당 객체를 호환하는 다른 객체를 만들어야합니다. 아래와 같이 말이죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HrFrontReturnVo {
    private Integer memberSubjectSn;        // 프론트에서 원하는 정보1
    private String name;                    // 프론트에서 원하는 정보2
 
    public Integer getMemberSubjectSn() {
        return memberSubjectSn;
    }
 
    public void setMemberSubjectSn(Integer memberSubjectSn) {
        this.memberSubjectSn = memberSubjectSn;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}
 
cs


즉 우리는 두 객체의 호환을 맞춰 주기 위해 모델에 호환 알고리즘을 작성해야합니다.


1
2
3
4
5
HrSampleVo sampleVo = new HrSampleVo();
        
HrFrontReturnVo frontReturnVo = new HrFrontReturnVo();
frontReturnVo.setMemberSubjectSn(sampleVo.getMemberSubjectSn());
frontReturnVo.setName(sampleVo.getName());
cs


다 좋습니다. 모델 A 에서 해당 알고리즘을 작성을 하였는데, 모델B 에서도 같은 리즈가 생겼습니다. 결국 위의 알고리즘을 복사해서 또 써야겠군요....


좋지 않습니다. 복사한 알고리즘은 그 때 해결할 수 있겠지만, 변화가 생기면 다 변경해야합니다.    

우아하지 않아요. ㅡㅡ^


HrFrontReturnVo 로 변경해야하는 일은 HrSampleVo가 책임져야 할 일이지(단일 책임의 원칙),   여러 모델 클래스들이 할 일이 아닙니다.


즉 우리는 HrSampleVo 안에 아래와 같은 메소드를 추가해봅시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HrSampleVo {
    private Integer memberSubjectSn;        // 프론트에서 원하는 정보1
    private String name;                    // 프론트에서 원하는 정보2
    private String sampleResult;
    private Boolean maleYn;
 
    /**
     * HrFrontReturnVo 으로 변환
     * 
     * @return
     */
    public HrFrontReturnVo toHrFrontReturnVo() {
        HrFrontReturnVo newVo = new HrFrontReturnVo();
        newVo.setMemberSubjectSn(memberSubjectSn);
        newVo.setName(name);
        
        return newVo;
    }
}
 
HrSampleVo sampleVo = new HrSampleVo();
 
// 간단한 데이터 변형, 어느 모델에서든 HrSampleVo 을 사용한다면 HrFrontReturnVo 로 변경가능
HrFrontReturnVo frontReturnVo = sampleVo.toHrFrontReturnVo();
cs


위와 같은 VO 내부에 객체변형메소드를 추가하였습니다. 


즉 어느 모델에서든 변환 알고리즘을 사용할 수 있으며, 변경이 필요할 시, toHrFrontReturnVo 만 수정하면 됩니다.


하지만, ORM 객체는 변활 할 수 없는 문제가 있네요. 


ORM 객체 중 MYBATIS 의 경우 제너레이트 버튼을 한번 누르면 작성한 로직들이 모두 사라집니다. 즉 저런식으로 객체 내부에 메소드를 사용할 수 없어보이는데요. 하지만 걱정할 것은 없습니다. 패턴이란 정해진 것이 아니니까요.


제너레이트 과정을 통해 아래와 같은 MemberGenerateSimple 이 만들어졌고, 프론트로 넘기기 위한 HrFrontReturnVo 를 제작한다고 가정합시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class MemberGenerateSimple {
    private Integer memberSubjectSn;
    private String name;
    private Integer companySubjectSn;
    private Integer age;
    private Boolean maleYn;
 
    public Integer getMemberSubjectSn() {
        return memberSubjectSn;
    }
 
    public void setMemberSubjectSn(Integer memberSubjectSn) {
        this.memberSubjectSn = memberSubjectSn;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public Integer getCompanySubjectSn() {
        return companySubjectSn;
    }
 
    public void setCompanySubjectSn(Integer companySubjectSn) {
        this.companySubjectSn = companySubjectSn;
    }
 
    public Integer getAge() {
        return age;
    }
 
    public void setAge(Integer age) {
        this.age = age;
    }
 
    public Boolean getMaleYn() {
        return maleYn;
    }
 
    public void setMaleYn(Boolean maleYn) {
        this.maleYn = maleYn;
    }
}
 
public class HrFrontReturnVo {
    private Integer memberSubjectSn;        // 프론트에서 원하는 정보1
    private String name;                    // 프론트에서 원하는 정보2
 
    // 제너레이트된 객체를 인자로 받아, 본인을 생성하는 정적 메소드 제작!
    public static HrFrontReturnVo CreateByMemberGenerateSimple(MemberGenerateSimple memberGenerateSimple) {
        HrFrontReturnVo newVo = new HrFrontReturnVo();
        newVo.memberSubjectSn = memberGenerateSimple.getMemberSubjectSn();
        newVo.name = memberGenerateSimple.getName();
 
        return newVo;
    }
}
 
MemberGenerateSimple memberGenerateSimple = new MemberGenerateSimple();
 
// 제너레이트 된 객체도 쉽게 변형가능. 객체의 생성을 숨기는 작업은 꽤 많은 편의를 줄 수 있습니다.
HrFrontReturnVo frontReturnVo = HrFrontReturnVo.CreateByMemberGenerateSimple(memberGenerateSimple);
cs


HrFrontReturnVo 를 직접 생산하지 않고, 정적메소드를 통해 특정목적에 맞게 생성하도록 하였습니다. 


만약 HrFrontReturnVo 가 오직 MemberGenerateSimple 객체를 통해서만 생성되길 원하고 남용되는 것을 막고자 한다면 아래와 같이 setter 나 생성자를 봉쇄함으로써(초기화 관련 작업 모두 제거), 다른 개발자들이 HrFrontReturnVo 사용하는 것을 막을 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public final class HrFrontReturnVo {
    private Integer memberSubjectSn;        // 프론트에서 원하는 정보1
    private String name;                    // 프론트에서 원하는 정보2
    
    // 자체적인 객체 생성 막기
    private HrFrontReturnVo(){}
 
    // 제너레이트된 객체를 인자로 받아, 본인을 생성하는 정적 메소드 제작!
    public static HrFrontReturnVo CreateByMemberGenerateSimple(MemberGenerateSimple memberGenerateSimple) {
        HrFrontReturnVo newVo = new HrFrontReturnVo();
        newVo.memberSubjectSn = memberGenerateSimple.getMemberSubjectSn();
        newVo.name = memberGenerateSimple.getName();
 
        return newVo;
    }
 
    // 오직 getter 만 사용가능하며, 생성된 인스턴스는 오직 read 만 가능함.
    // vo 활용의 오남용 방지
    
    public Integer getMemberSubjectSn() {
        return memberSubjectSn;
    }
    
    public String getName() {
        return name;
    }
}
 
cs


이것으로 4월 10일에 pdf 와 같이 수업했던 내용의 정리를 마칩니다.

궁금한 것은 댓글이나, 찾아올 것 :-)

반응형
Posted by N'