저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링4 입문

스프링 4

DDD Start

객체 지향과
디자인 패턴

JSP 2.3

JPA 입문

기존 코드의 중복 코드: 흐름 제어 중복


최근에 정리한 코드는 다음과 같은 형태를 가졌다.


private ErrorLogger errorLogger;


public Response chooseMeeting(Request req) {

    String apiId = "E0004";

    String code = null;

    String jupno = null;

    String transt = null;

    try {

        jupno = request.getJupno();

        changeTranst(jupno, transt = "0", req.getId(), req.getIp()); // 전송전 상태

        Result result = tranService.chooseMeeting(req); // 전송 처리

        code = eocsResult.getCode();

        changeTranst(jupno, transt = "1", req.getId(), req.getIp()); // 전송완료 상태

    }catch (EocsException ex) {

        transt(jupno, transt = "2", req.getId(), request.getIp()); // 전송오류 상태

        code = ex.getEocsErrorCode();

        errorLogger.append(apiId, code, jupno, transt, req, req.getId(), req.getIp(),....);

    }catch (Exception e) {

        code = "C00003";

        errorLogger.append(apiId, code, jupno, transt, req, req.getId(), req.getIp()....);

    }

    return new Response(apiId , code);

}


private changeTranst(String jupno, String transt, String id, String ip) {

    ...

}


chooseMeeting() 메서드는 다음 흐름을 갖는다.

  1. 전송전 상태 변경: changeTranst()
  2. 실제 전송 업무 처리: tranService.chooseMeeting()
  3. 전송완료 상태 변경: changeTranst()
    1. 전송오류 발생시 전송오류 상태 변경 후 로그 기록
    2. 이 외 익셉션 로그 기록
  4. 결과 리턴

chooseMeeting() 메서드만 이런 코드를 가진 게 아니다. 이와 동일한 흐름을 갖는 메서드가 3개가 더 있었다. 차이점은 apiId와 전송 처리 코드뿐이었다. 예를 들어, 다른 코드는 아래와 같다.


public Response choosePip(PipRequest req) {

    String apiId = "E0003";

    String code = null;

    String jupno = null;

    String transt = null;

    try {

        jupno = request.getJupno();

        changeTranst(jupno, transt = "0", req.getId(), req.getIp()); // 전송전 상태

        Result result = tranService.choosePip(req); // 전송 처리

        code = eocsResult.getCode();

        changeTranst(jupno, transt = "1", req.getId(), req.getIp()); // 전송완료 상태

    }catch (EocsException ex) {

        transt(jupno, transt = "2", req.getId(), request.getIp()); // 전송오류 상태

        code = ex.getEocsErrorCode();

        errorLogger.append(apiId, code, jupno, transt, req, req.getId(), req.getIp(),....);

    }catch (Exception e) {

        code = "C00003";

        errorLogger.append(apiId, code, jupno, transt, req, req.getId(), req.getIp()....);

    }

    return new Response(apiId , code);

}



메서드 추출로 하려다가...


처음엔 대충 다음과 같은 모양을 상상하면서 메서도 추출로 가려했다.


public Response chooseMeeting(Request req) {

    String apiId = "E0004";

    return runTrans(apiId, req.getJupno(), req.getId(), req.getIp(), 

                         () -> tranService.chooseMeeting(req));

}


public Response choosePip(PipRequest req) {

    String apiId = "E0003";

    return runTrans(apiId, req.getJupno(), req.getId(), req.getIp(), 

                         () -> tranService.choosePip(req));

}


private Response runTrans(String apiId, 

                          String jupno, String id, String ip, // 상태처리나 로그 기록에 필요

                          Supplier<Result> transition) {

    String code = null;

    String transt = null;

    try {

        jupno = request.getJupno();

        changeTranst(jupno, transt = "0", id, ip); // 전송전 상태

        Result result = transition.get(); // 전송 처리

        code = eocsResult.getCode();

        changeTranst(jupno, transt = "1", id, ip); // 전송완료 상태

    }catch (EocsException ex) {

        transt(jupno, transt = "2", id, ip); // 전송오류 상태

        code = ex.getEocsErrorCode();

        errorLogger.append(apiId, code, jupno, transt, id, ip,....);

    }catch (Exception e) {

        code = "C00003";

        errorLogger.append(apiId, code, jupno, transt, id, ip,....);

    }

    return new Response(apiId , code);

}


private changeTranst(String jupno, String transt, String id, String ip) {

    ...

}


runTrans() 메서드는 로그를 남기거나 상태 변경을 처리하는 코드에서 필요한 값을 파라미터로 받는데, 파라미터가 다소 많다. 로그를 남길 때 사용할 jupno, id, ip를 구하는 코드가 달라서, 이 세 개 값을 파라미터로 전달해야 했다.


이렇게 되면 코드를 추가로 정리할 때 번잡함이 발생한다. 예를 들어, runTrans()에서 changeTranst()에 의미를 더 부여하기 위해 다음과 같이 변경한다고 하자.


private Response runTrans(String apiId, String jupno, 

                                   String id, String ip, Object req, Supplier<Result> transition) {

    String code = null;

    String transt = null;

    try {

        jupno = request.getJupno();

        transt = beforeTranst(jupno, id, ip); // 전송전 상태

        Result result = transition.get(); // 전송 처리

        code = eocsResult.getCode();

        transt = afterTranst(jupno, id, ip); // 전송완료 상태

    }catch (EocsException ex) {

        errorTranst(jupno, id, ip); // 전송오류 상태

        code = ex.getEocsErrorCode();

        errorLogger.append(apiId, code, jupno, transt, req, id, ip,....);

    }catch (Exception e) {

        code = "C00003";

        errorLogger.append(apiId, code, jupno, transt, req, id, ip,....);

    }

    return new Response(apiId , code);

}


private String beforeTranst(String jupno, String id, String ip) {

    changeTranst(jupno, "1", id, ip);

    return "1";

}

...afterTranst()와 errorTranst()도 비슷하게 구현


private changeTranst(String jupno, String transt, String id, String ip) {

    ...

}


runTrans() 메서드는 에러 로그를 기록하는데 transt 로컬 변수를 사용한다. 그래서 beforeTranst() 메서드는 상태 변경 실행후 transt 로컬 변수에 할당할 값을 리턴해야 한다. 게다가 beforeTranst() 메서드는 changeTranst() 메서드를 호출하기 위해 필요한 값을 파라미터 3개로 받고 있다.


흐름 처리를 수행하는 클래스 작성


메서드 추출로 중복을 없앨 수는 있지만, 그 결과로 만들어진 코드가 이쁘지 않다. 로컬 변수나 파라미터 개수가 많기에 이를 별도 객체로 뽑아서 중복을 없애는 것으로 방법을 바꿨다. 먼저 다음과 같이 프로세스를 처리하는 클래스를 만들었다.


public class TransitionProcess {

    private ErrorLogger errorLogger;


    private String apiId;

    private String jupno;

    private String id;

    private String ip;


    private String transt;

    private String code;


    @Builder

    public TransitionProcess(

            ErrorLogger errorLogger, String apiId,

            String jupno, String id, String ip) {

        this.errorLogger = errorLogger;

        this.apiId = apiId;

        this.jupno = jupno;

        this.id = id;

        this.ip = ip;

    }

    

    public Response runWith(Supplier<Result> transition) {

        try {

            beforeTranst();

            Result result = transition.get();

            code = result.getCode();

            afterTranst();

        } catch (EocsClientException ex) {

            transtError();

            code = ex.getEocsErrorCode();

            appendErrorLog(ex);

        } catch (Exception ex) {

            code = "C00003";

            appendErrorLog(ex);

        }

        return new Response(apiId , code);

    }


    private void beforeTranst() {

        transt = "0";

        changeTranst();

    }


    ...


    private void changeTranst() {

        ... // apiId, jupno, code, transt 등 필요한 값이 필드에 존재

    }


    private void appendError(Exception ex) {

        errorLogger.append(apiId, code, jupno, transt, id, ip, ex);

    }

}


TransitionProcess 클래스는 생성자를 통해서 로그 기록이나 상태 변경에 필요한 값을 받는다. runWith() 메서드는 전송 처리 기능을 함수형 인터페이스로 전달받는다.


상태 변경을 수행하는데 필요한 값(jupno, id, ip, transt)이 필드에 존재하므로 changeTranst() 메서드는 파라미터가 필요없다. 동일하게 beforeTranst() 메서드나 afterTranst() 메서드도 파라미터가 필요 없다.


에러 로그를 남기기 위한 appendError()도 동일하게 에러 로그를 기록하는데 필요한 값 중 Exception을 제외한 나머지는 필드에 존재한다. 그래서 파라미터로 Exception만 받으면 된다.


사실, TransitionProcess의 beforeTrans(), afterTrans(), errorTrans(), appendError() 메서드는 처음부터 존재한 것이 아니고 클래스로 추출한 뒤에 리팩토링하는 과정에서 생긴 것이다. 파라미터로 필요한 값을 전달할 필요가 없기 때문에 코드를 정리하기 더 쉽고, 메서드 호출에 파라미터가 없으므로 코드도 덜 복잡하다.


결과


다음은 흐름 처리 객체를 이용햇서 변경한 코드이다.


public Response chooseMeeting(Request req) {

    String apiId = "E0004";

    TransitionProcess process = TransitionProcess.builder()

        .errorLogger(errorLogger)

        .apiId(apiId)

        .jupno(req.getJupno())

        .id(req.getId())

        .ip(req.getIp())

        .build();

    return process.runWith(() -> tranService.chooseMeeting(req));

}


public Response choosePip(PipRequest req) {

    String apiId = "E0003";

    TransitionProcess process = TransitionProcess.builder()

        .errorLogger(errorLogger)

        .apiId(apiId)

        .jupno(req.getJupno())

        .id(req.getId())

        .ip(req.getIp())

        .build();

    return process.runWith(() -> tranService.choosePip(req));

}


// changeTranst 메서드 제거. TransitionProcess로 이동.


개인적으로는 처음 메서드 추출로 중복을 제거하려고 했던 것보다 더 깔끔하고 프로세스 처리가 별도 클래스로 빠져서 프로세스 분석이나 수정이 용이해진 것 같다.

Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

개발 중인 서비스에서 통합 테스트를 수행하려면 외부 시스템과의 상호 작용을 통해서만 정상적으로 동작하는 기능이 존재한다. 다음은 그 예이다.(E는 외부 시스템과 연동이 필요한 기능을 의미)

  • 접수함(E) > [배관유무확인요청1상태] > 알림벨(E) > [배관유무확인요청2상태] > 배관있음(E) > [표시/회합 결정대기상태]
여기서 특정 데이터를 표시/회합결정대기상태로 만들려면 내부 시스템의 데이터만 바꿔서는 안 된다. 외부에 있는 시스템의 기능을 순차적으로 실행해야 해당 상태로 바꿀 수 있다. 접수함 행위를 외부 시스템에서 하지 않으면 [배관유무확인요청1상태]로 바뀌지 않는다.

외부 시스템과 연동없이 내부 데이터만 [배관유무확인요청1상태]로 바꿔 알림벨 기능을 통합 테스트하면 정상 동작하지 않는다. 외부 시스템의 데이터 상태가 유효하지 않아 외부 시스템이 에러를 발생하기 때문이다.


통합 테스트나 QA를 하려면 수시로 데이터를 특정 상태로 변경해야 했다. 특정 상태로 변경하려면 올바른 순서대로 기능을 실행해야했는데, 이를 위해 다음과 같이 상태를 변경할 수 있는 코드를 테스트 영역에 추가했다.


EocsTransitionRunner runner = ...;

EocsTransitions trans = EocsTransitionsBuilder.builder()

        .receive()

        .alimBellOfNormalJobgu()

        .assignBlock(...)

        .pipeYes()

        .needMeetJobpr20()

        .lipynA()

        .build();


runner.run(trans, jupno, empId);


상태를 변경하는 것을 Transition(전이)로 표현했고, EocsTransitions은 특정 상태로 가기 위한 Transition 목록을 담는다. 메서드 호출로 전이를 표현했고 상태에 따라 알맞은 전이 목록을 생성하도록 했다.


예를 들어, receive() 메서드는 접수를 의미하고, receive() 다음에는 alimBellOfNormalJob()이나 alimBellOfSmallOrEmergent()만 호출할 수 있게 했다. alimBellOfNormalJob() 메서드 호출 다음에는 assignBlock()만 호출 가능하고, 이어서 pipeYes()나 pipeNo()만 호출 가능하게 했다.


EocsTransitionsBuilder는 상태 전이를 어떻게 할지를 기술하는 EocsTransitions를 생성하고 상태 전이는 EocsTransitionRunner라는 별도의 실행기로 처리했다. 그래서 EocsTransitionRunner#run()을 호출하지 않으면 실제 상태 전이는 발생하지 않는다.


EocsTransitions과 EocsTransition


EocsTransitions과 EocsTransition은 간단한 전이 과정을 담기 위한 간단한 클래스이다.


EocsTransition은 다음과 같다.


public class Transition {


    private TranstionType type;

    private Map<String, String> props = new HashMap<>();

    

    public Transition(TranstionType type) {

        this.type = type;

    }

    

    public TranstionType type() {

        return type;

    }


    public Transition addProp(String prop, String value) {

        props.put(prop, value);

        return this;

    }


    public String getProp(String prop) {

        return props.get(prop);

    }


    public String getPropOrEmpty(String prop) {

        String value = getProp(prop);

        return value == null ? "" : value;

    }

}


상태 전이에 따라 추가적인 데이터가 필요한데 이를 props라는 Map에 담도록 했다.


TransitionType은 열거 타입으로 각 전이를 값으로 표현한다.


public enum TranstionType {


    RECEIVE, 

    ALIMBELL, ALIMBELL_OF_SMALL_OR_EMERGENT,

    ASSIGN_BLOCK,

    PIPE_Y, PIPE_N, PIPE_S, PIPE_T,

    JOBPR_20, JOBPR_40, JOBPR_50,

    LIPYN_A, LIPYN_B, LIPYN_C, LIPYN_D, LIPYN_Y, LIPYN_N, 

    

    EOCS_POS_DISP, EOCS_COMPLETE,

}


EocsTransitions는 Transition 목록을 담는 간단한 클래스이다.


public class EocsTransitions {


    private List<Transition> transitions;


    public EocsTransitions(List<Transition> transitions) {

        this.transitions = Collections.unmodifiableList(transitions);

    }


    public List<Transition> transitions() {

        return transitions;

    }

}


상태 전이 생성 위한 EocsTransitionsBuilder와 관련 클래스


EocsTransitionsBuilder와 관련 클래스 일부를 아래 표시했다.




최초 상태는 S20PipeConfirmBuilder인데, 이는 EocsTransitionBuilder의 정적 메서드인 receive()가 생성한다.


EocsTransitionsBuilder.receive() // S20PipeConfirmBuilder 리턴


AbstractTransitionBuilder를 상속받은 각 클래스는 특정 상태에서 가능한 전이만을 제공한다. 예를 들어, S30PipeConfirmBuilder는 상태 값이 "30"인 배관유무확인2 상태에서 선택할 수 있는 전이 네 개(pipeYes, pipeNo, pipeSelf, pipeTest)를 제공한다. 비슷하게 S30PipeConfirmBeforeAssignBlockBuilder는 한 개의 상태 전이인 assignBlock()만 제공한다.


전이를 위한 메서드는 다음 상태를 위한 Transition Builder를 리턴한다. 각 Transition Builder는 해당 상태에서 가능한 전이만 제공하므로, 잘못된 전이 경로를 설정할 수 없다. 예를 들어, S20PipeConfirmBuilder의 alimBellOfNormalJobgu() 메서드는 S30PipeConfirmBeforeAssignBlockBuilder를 리턴하므로 alimBellOfNormalJobggu() 메서드 호출 다음에는 assignBlock() 메서드만 호출할 수 있다. 다른 메서드는 호출할 수 없다.


EocsTransitions trans = EocsTransitionsBuilder

    .receive() // S20PipeConfirmBuilder 리턴

    .alimBellOfNormalJobgu() // S30PipeConfirmBeforeAssignBlockBuilder 리턴

    .assignBlock(...) // S30PipeConfirmBuilder 리턴

    .pipeYes() // S40DispMeetWaitBuilder 리턴

    .build();



EocsTransitionsBuilder 클래스


public class EocsTransitionsBuilder {


    public static EocsTransitionsBuilder builder() {

        return new EocsTransitionsBuilder();

    }


    private List<Transition> transitions = new ArrayList<>();


    public S20PipeConfirmBuilder receive() {

        addTransition(new Transition(TranstionType.RECEIVE));

        return new S20PipeConfirmBuilder(this);

    }


    public void addTransition(Transition transition) {

        transitions.add(transition);

    }


    public EocsTransitions build() {

        return new EocsTransitions(transitions);

    }

}


EocsTransitionsBuilder 클래스의 receive() 메서드는 TransitionType.RECEIVE을 값으로 갖는 최초 상태 전이를 추가하고, 첫 번째 상태를 위한 S20PipeConfirmBuilder 객체를 리턴한다.


AbstractTransitionBuilder 클래스


AbstractTransitionBuilder은 각 Transition Builder가 필요로 하는 기능을 제공한다.


public static abstract class AbstractTransitionBuilder {

    private EocsTransitionsBuilder builder;


    public AbstractTransitionBuilder(EocsTransitionsBuilder builder) {

        this.builder = builder;

    }


    public final EocsTransitions build() {

        return builder.build();

    }


    protected final void addTransition(Transition transition) {

        builder.addTransition(transition);

    }


    protected final EocsTransitionsBuilder getBuilder() {

        return builder;

    }

}


build() 메서드는 생성자로 전달받은 EocsTransitionsBuilder의 build()를 호출하는데, 이 메서드가 있어 전이 생성 과정에서 언제든지 EocsTransitions를 생성할 수 있다.


개별 Transition Builder 클래스


개별 Transition Builder 클래스는 AbstractTransitionBuilder 클래스를 상속받아 구현했고, 상태 전이를 위한 메서드를 제공했다. 상태 전이 메서드는 addTransition() 메서드를 이용해서 알맞은 상태 전이를 추가하고 다음 Transition Builder를 리턴한다. 다음은 예이다.


public static class S20PipeConfirmBuilder extends AbstractTransitionBuilder {


    public S20PipeConfirmBuilder(EocsTransitionsBuilder builder) {

        super(builder);

    }


    public S30PipeConfirmBeforeAssignBlockBuilder alimBellOfNormalJobgu() {

        addTransition(new Transition(TranstionType.ALIMBELL));

        return new S30PipeConfirmBeforeAssignBlockBuilder(getBuilder());

    }


    public S60EnterWaitBeforeAssignBlockBuilder alimBellOfSmallOrEmergent() {

        addTransition(new Transition(TranstionType.ALIMBELL_OF_SMALL_OR_EMERGENT));

        return new S60EnterWaitBeforeAssignBlockBuilder(getBuilder());

    }

}


최종 상태를 위한 Transition Builder는 더 이상 진행할 전이가 없으므로 다음과 같이 전이를 위한 메서드가 없다.


public static class S90DoneBuilder extends AbstractTransitionBuilder {

    public S90DoneBuilder(EocsTransitionsBuilder builder) {

        super(builder);

    }

}


실행기

실행기는 다음과 같다..


public class EocsTransitionRunner {

    public EocsTransitionRunner(...의존) {

        ...의존주입

    }


    public void run(EocsTransitions trans, String jupno, String empId) {

        trans.transitions().forEach(tran -> {

            runTransition(tran, jupno, empId);

        });

    }


    private void runTransition(Transition tran, String jupno, String employeeId) {

        switch (tran.type()) {

        case RECEIVE:

            initEocs(jupno, DigWorkPathFlag.EOCS_1.cd());

            break;

        case ALIMBELL:

        case ALIMBELL_OF_SMALL_OR_EMERGENT:

            alimBell(jupno, employeeId, "01012345678")

            break;

        case ASSIGN_BLOCK:

            assignBlock(tran, jupno, employeeId);

            break;

        case PIPE_Y:

        ...생략

        case PIPE_T:

            doPipeynProcess(jupno, employeeId, tran);

            break;

        case JOBPR_20:

        case JOBPR_40:

        case JOBPR_50:

            doJobprProcess(jupno, employeeId, tran);

            break;

        case LIPYN_A:

        ...생략

        case LIPYN_N:

            doLipynProcess(jupno, employeeId, tran);

            break;

        case EOCS_POS_DISP:

            unsupport(tran.type());

            break;

        case EOCS_COMPLETE:

            skip(tran.type());

            break;

        }

    }


run() 메서드는 EocsTransitions에 보관된 Transition 목록을 구해서 차례대로 전이를 실행한다. runTransition() 메서드는 각 전이 타입에 따라 알맞은 기능을 실행해서 상태를 알맞게 변경한다. 상태 전이를 수행하는데 필요한 의존은 생성자를 통해서 전달받았다.


이제 데이터를 특정 상태로 맞추고 싶으면 다음과 같은 코드를 사용해서 상태를 변경할 수 있다.


EocsTransitionRunner runner = new EocsTransitionRunner(...);


EocsTransitions data1Trans = EocsTransitionsBuilder.builder() // 전이 정의

        .receive()

        .alimBellOfNormalJobgu()

        .assignBlock(...)

        .pipeYes()

        .build();

runner.run(data1Trans, jupno1, empId); // 전이 실행


EocsTransitions data2Trans = EocsTransitionsBuilder.builder() // 전이 정의

        .receive()

        .build();


runner.run(data2Trans, jupno2, empId); // 전이 실행


각 상태별로 알맞게 전이를 지정할 수 있게 하려고, 상태마다 클래스를 추가했지만, 상태 초기화하는 코드를 보면 쉽게 상태 초기화가 어떤 과정으로 이루어지는지 알 수 있게 되었고, 특정 상태로 맞추는 코드 역시 코드 자동 완성 기능으로 쉽게 정의할 수 있게 되었다.


Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.