JAVA/우아한테크코스 4기

[220512] Spring - 지하철 노선도 미션을 통해 배운 점

kth990303 2022. 5. 12. 17:36
반응형

이번 미션은 총 3단계로 구성돼있다.

 

1단계 - 지하철역/노선 관리 기능 (API 및 E2E test 구현에 익숙해지자)

  • StationController를 참고하여 LineController 작성 및 비즈니스 로직 구현
  • Spring annotation 사용 불가! (@RestController 제외)
  • JdbcTemplate 사용 불가 및 static Dao 자료구조 사용
  • E2E test 작성하기

 

2단계 - 프레임워크 적용 (스프링을 적용해보자)

  • 스프링 빈과 스프링 Jdbc 사용
  • H2 database 사용

 

3단계 - 지하철 구간 관리 기능 (요구사항이 추가될 때를 경험해보자)

  • 구간 등록/삭제 기능
    • 구간 상행 등록, 하행 등록, 중간 등록, 상행 종점 삭제, 하행 종점 삭제, 중간 지점 삭제, 환승역 고려 등 구간의 등록 삭제가 자유로워야 함.
    • 노선을 등록할 때 구간도 자유롭게 등록돼야 함.

이번 미션은 특히나 3단계가 정말 빡셌다. 구간을 등록하고 삭제하는 과정에서 수많은 경우의 수가 존재했기 때문이다. 구현량도 구현량이지만, 이를 어떻게 객체지향적으로 설계할지와 db로직과 domain 로직의 조화를 생각하면서 코드를 구현하는 과정 때문에 많은 크루들이 이 미션을 '지옥철 미션'이라고 부르게 됐다.

 

하지만 그만큼 얻어갈 점도 많은 미션이었다. 이번 포스팅에선 내가 얻어간 점에 대해 간단하게 작성해보려 한다.

내 code 및 PR은 여기서 볼 수 있다.

https://github.com/woowacourse/atdd-subway-map/pull/259

 

[Spring 지하철 노선도 - 3단계] 케이(김태현) 미션 제출합니다. by kth990303 · Pull Request #259 · woowacours

안녕하세요 제이 😄 코로나 확진돼버리는 바람에 집에만 있다보니 생각보다 미션을 빨리 제출하게 됐습니다 ㅎㅎ 모든 기능은 API 문서 v2를 따르게 했습니다. 그렇기 때문에 프론트 화면에서

github.com


스프링 빈과 스프링 Jdbc의 편리성 (1단계 VS 2단계)

1단계에선 요구사항에 따라 스프링 빈과 스프링 Jdbc를 사용하지 못했다. 그렇기 때문에 dao 로직을 실행할 자료구조를 만들어주었다. 이 자료구조는 domain을 List로 담고 있으며, static id를 상태로 가지고 있다. 모든 메서드는 static으로 구성돼있다. (객체지향과 조금 거리가 있게 된다.) 하지만, 2단계에서는 스프링 빈과 스프링 Jdbc를 사용할 수 있었다.

 

1단계의 dao 코드와 2단계의 JdbcDao 코드를 비교해보자.

1단계 dao 코드

스프링 Jdbc를 사용하지 못하고 List<Station>을 상태로 가지므로 stream으로 findById를 진행해준 후, update는 도메인에서 setter method (!!!)를 이용해서 하고 있다. 특정 도메인을 update하는 과정은 sql 쿼리문을 이용한다면 where절로 바로 진행할 수 있지만, 1단계 dao 코드로는 stream으로 해당 도메인을 찾아서 직접 update를 해줘야 했다.

 

심지어 update가 필요한 메서드기 때문에 도메인에서 세터 메서드를 이용해야 함으로써, 도메인의 프로퍼티 값이 언제든지 변할 수 있는 위험에 처하게 됐다.

2단계 dao 코드

스프링 Jdbc를 사용할 수 있게 되어 sql 쿼리문 하나로 작업이 끝나게 됨을 알 수 있다! 또한, 도메인에 의존하지 않고 원하는 작업을 실행할 수 있었다.  


테스트 격리 방법 중 하나, @DirtiesContext

아래 테스트 코드를 보자.

@SpringBootTest를 붙여주어 application을 직접 실행시키지 않고도, 실행 환경과 유사하게 진행할 수 있도록 Application Context를 제공받을 수 있다. @SpringBootTest에서 제공해주는 기능들을 이용하면 Port 지정 및 mockMvc test 또한 가능케 해준다.

 

그런데 그 위에 있는 @DirtiesContext는 뭘까?

바로 테스트 격리를 위해 각 테스트 케이스마다 컨텍스트를 재생성해주는 어노테이션이다. 테스트들은 순서에 상관없이 독립적으로 수행돼야 한다. 따라서 테스트 격리가 이루어져야 한다. 그 이유로, 각 테스트마다 같은 자원 (ex. database)를 공유하기 때문에 테스트 격리가 제대로 되지 않는다면 독립적으로 수행될 수 없기 때문이다. 따라서 매 테스트마다 기존 애플리케이션 컨텍스트를 폐기하고 새로운 컨텍스트를 재생성해주도록 @DirtiesContext를 붙여주어야 한다.

@DirtiesContext가 없으면 테스트 격리가 이루어지지 않는다.

위 failed test는 단일 테스트로 실행하면 success되지만, run all tests를 진행할 경우 failed가 뜨는 테스트들이다. 공유하는 db 자원이 존재하기 때문에 의도치 않은 결과를 발생시키는 것이다. @DirtiesContext를 사용하면 해결된다.


E2E Test에서 테스트를 최대한 꼼꼼하게!

이번 미션에서는 End to End 테스트를 진행했다. 그동안 나는 단위 테스트만 진행했고, api test는 진행하는 방법을 몰라 작성하지 않았었다. 하지만 이번 미션으로 RestAssured를 이용하는 e2e test를 배우게 돼 api 호출 시 응답이 원하는대로 들어가는지, http status code는 의도대로 담기는지 테스트할 수 있었다.

 

RestAssured 의 given()에 요청을, when()에 api 지정을, then()에 응답을 받아 테스트를 진행할 수 있다.

given(), then()에 .log().all()을 붙여줄 경우, 요청 객체, 응답 객체를 콘솔에 띄워주기 때문에 의도한 대로 돌아가는지 쉽게 확인할 수 있다. when()에 api 지정을 할 때는 ContentType, Http method, URI를 올바르게 작성해주어야 한다. 잘 작성해주지 않는다면 에러를 발견하게 된다.

(에러 발견 예시: https://kth990303.tistory.com/320 , https://kth990303.tistory.com/321 )

 

자세한 RestAssured 사용법은 아래 포스팅을 참고하자.

https://kth990303.tistory.com/315

 

[Spring][TDD] RestAssured를 이용한 e2e test로 Controller API까지 통합 테스트해보자

그동안 나는 단위 테스트만을 진행해왔다. 도메인 로직이 잘 실행되는지 junit 문법의 Assertions로 테스트해왔다. dao test는 @JdbcTest를 이용하여 단위테스트를 했고, service test는 fake 객체를 만들어주

kth990303.tistory.com

 

RestAssured와 org.hamcrest.Matchers 를 함께 이용해주어 assertThat 대신 테스트를 진행할 수도 있다.
또한, 객체의 값이 동일한지 체크해주기 위해 AssertJ 문법의 usingRecursiveComparison 을 이용해주는 방법도 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void findSectionsByDeleteLine(Long lineId, StationResponse stationResponse1,
                                      StationResponse stationResponse2,
                                      StationResponse stationResponse3) {
    ExtractableResponse<Response> response = RestAssured.given().log().all()
            .when()
            .get("/lines/" + lineId)
            .then().log().all()
            .extract();
    checkByDeleteValidSections(lineId, response, stationResponse1, stationResponse2, stationResponse3);
}
 
private void checkByDeleteValidSections(Long lineId, ExtractableResponse<Response> response,
                                        StationResponse stationResponse1,
                                        StationResponse stationResponse2,
                                        StationResponse stationResponse3) {
    final List<StationResponse> stationResponses =
            List.of(stationResponse1, stationResponse2, stationResponse3);
    final LineResponse expected = new LineResponse(lineId, 이호선.getName(), 이호선.getColor(), stationResponses);
    final LineResponse actual = response.jsonPath().getObject(".", LineResponse.class);
 
    assertThat(actual)
            .usingRecursiveComparison()
            .isEqualTo(expected);
}
cs

위 코드는 구간 삭제 시, 남아있는 지하철역들을 RestAssured를 이용한 api 응답값으로 조회하고 있다.

usingRecursiveComparison 문법을 이용하여 객체의 주소값이 달라도, 프로퍼티 값들을 비교하여 일치할 경우 success를 반환한다.

 

???: domain, dao, service test로 충분히 테스트가 됐는데, 또 테스트를 작성해야 하는가?

 

나는 작성하는 것이 좋다는 결론을 내렸다. 도메인, 서비스 테스트가 모두 통과가 된다 하더라도, 컨트롤러 코드를 잘못 작성하여 의도치 않은 응답값을 반환할 수도 있기 때문이다. 또한, e2e test는 이 api를 사용할 때 문제가 없다는 것을 최종적으로 확인하기에도 적합하다. api를 사용하는 개발자 혹은 유저들이 우리의 도메인, 서비스 코드를 이용하는게 아니고, 결국은 우리가 만든 api를 사용하는 것이기 때문이다. 따라서 이 api를 호출했을 때, 최종적으로 응답값을 검증하는 e2e 테스트는 꼭 필요하다는 결론을 내렸다. 그리고 도메인, 서비스 테스트가 충분하지 않은 경우일 때 e2e test에서 검증이 되는 경우도 존재할 수 있다. (실제로 내가 그랬다...) 인간은 실수를 저지르는 동물이기 때문에 자신이 천재가 아니라면 꼼꼼히 테스트를 작성하는 것을 추천한다.


E2E Test와 Service Test, Domain/Dao Test

그렇다면 반대로 e2e test가 존재하는데 굳이 service test나, 그보다 하위 layer의 domain, dao test를 작성할 필요가 있는지 의문을 품을 수도 있다. 실제로 처음에 나는 service test의 필요성을 느끼지 못했었다. domain, dao test에서 각 도메인로직과 dao 로직이 충분히 검증된다고 판단했고, e2e test를 통해 api 응답값도 검증이 됐으니, 자동으로 service test는 검증이 됐다고 판단했기 때문이다.

 

이와 관련해 리뷰어 제이와 의견을 나눈 후, 정리한 내 생각을 적어보았다.

Before.
웬만한 내부 로직은 domain test나 dao test에서 처리가 된다고 생각하고, 실제 api 테스트는 controller test로 e2e test를 통해 검증이 된다고 판단하여 service test는 일부 예외들만 검증하고 있습니다.
 (실제로 이번에 SectionAcceptanceTest (e2e test)를 꼼꼼하게 작성하는 데에 많은 시간을 쏟았습니다!)
현재 저의 service test가 양이 너무 적지는 않은지 궁금합니다. 실제로 SectionAcceptanceTest (e2e test)랑 domain test, dao test로 충분하다고 생각은 들지만, 서비스 테스트가 양이 좀 적어서 이게 맞는지 의문이 들더라고요.

After.
SectionAcceptanceTest (e2e test) 와 도메인 로직 테스트가 있다고 하더라도, 서비스 테스트에서 충분한 테스트가 필요하겠다는 생각을 제이의 의견을 듣고 저도 공감하게 되었습니다. 도메인 로직과 dao test를 해주고 있다고 하더라도, 서비스에서는 그 dao 로직들과 도메인 로직들을 이것저것 다루면서 어떠한 트랜잭션 단위의 기능을 해주고 있기 때문이에요! 

 

각 layer의 테스트를 꼼꼼히 작성하면, failed가 뜰 때 어디가 잘못됐는지 찾기가 쉬워진다. domain/dao 로직이 잘못됐는지, 아니면 이를 통합하여 트랜잭션 단위의 작업을 해주는 service 로직이 잘못됐는지, 아니면 controller를 잘못 작성했는지 확인이 편해지는 것이다. 또한, 테스트가 불필요하다는 것은 그 계층 클래스가 정말 필요한지 고민을 해보는 것도 좋다. 실제로 테스트가 불필요하다면, 그 클래스에서 하는 역할이 불필요한 것일 확률도 존재하기 때문이다.


Service Test에서 내가 mock 테스트를 진행하지 않은 이유

이번 미션의 구조에서 Service는 dao를 의존해야 했다. 따라서 Service test를 진행해주기 위해서는 dao 주입이 필요했다. 여기서는 총 3가지 방법이 존재한다.

  • db와 연동되지 않은 자료구조 구현체를 이용한 fakeDao 사용
  • 스프링 jdbc를 활용한 fakeDao 사용
  • mockMvc를 활용한 mock test

나는 첫번째 방법을 이용했다. 1단계에서 스프링 빈, 스프링 jdbc를 이용하지 않고 구현했었던 것을 재활용하고 싶었기 때문이다. 그뿐만 아니라, 이 방법을 택하면 별도로 jdbc와 연동이 되지 않기 때문에 실제 애플리케이션과 db를 공유하지 않고 독립적으로 실행할 수 있었기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Service Test
public class LineServiceTest {
    private final LineService lineService =
            new LineService(LineDaoImpl.getInstance(), StationDaoImpl.getInstance(), SectionDaoImpl.getInstance());
 
    @BeforeEach
    void setUp() {
        final List<Line> lines = LineDaoImpl.getInstance().findAll();
        lines.clear();
        final List<Station> stations = StationDaoImpl.getInstance().findAll();
        stations.clear();
        final List<Section> sections = SectionDaoImpl.getInstance().findAll();
        sections.clear();
    }
    // ...
}
cs

물론 jdbcTemplate을 활용한 fakeDao를 이용한 서비스 테스트도 괜찮다고 생각한다. 대신, 테스트 격리를 해주기 위해 @BeforeEach나 @Sql을 이용하여 설정을 잘 해주어야 할 것이다.

 

그렇지만, 이번 미션 한정으로 mockMvc 테스트를 이용하는 것은 약간 고민이 됐다.

이 부분 역시 리뷰어 제이의 의견을 듣고 생각을 정리한 내용을 적어보겠다.

J (리뷰어):
 impl이라고 만들어주신 코드가 테스트에서만 쓰이는거같아요. 의도한 결과를 내기 위해서 임시로 클래스를 만드셨을거에요. 이것을 제거하고 mocking을 사용하는것도 방법이 될 것 같아요. impl을 제거하고 이런 방식으로 해보는건 어떠신가요? 어느게 더 좋을지는 판단해보셔요.

K (나):
제이가 피드백해준대로 mock을 사용할지, 현재 방법대로 fakeDao를 이용할지 고민을 해봤어요. 두 방법 모두 좋다고 생각합니다!

다만, mock을 이용하면 실제 service 설계가 꽤 구현이 완성된 상태일 경우에만 test의 given을 작성할 수 있다고 생각했고, 그렇게 하면 이번 주제인 atdd 연습에는 맞지 않다고 생각해서 mock을 도입하지 않았습니다. 실제로 service는 그보다 한층 작은 단위의 layer인 dao를 의존하고 있는데, 그렇게 때문에 service test를 mock으로 할 경우, 앞에서 언급한대로 service 프로덕션 코드 설계가 꽤 이루어진 상태에서만 테스트 코드를 작성할 수 있다고 판단했어요. 그렇게 하면 정말 tdd가 맞을까? 고민도 많이 되기도 했구요. 이번에는 fakeDaoImpl들도 존재하기 때문에 현재 방법대로 테스트하는 것을 유지했습니다!

J (리뷰어):
넵 잘 판단해주셨어요! 필요할때 만드는게 제일 좋습니다 👍

 

실제로 mock을 활용한 test에서의 given을 작성하기 위해서는 서비스 프로덕션 코드의 구조가 어느정도 완성된 상태에서야 진행이 가능하다. 이는 test -> production 순으로 코드를 작성하는 설계 방법인 tdd와는 반대된다. 물론 프로덕션 코드를 제대로 작성했는지 검증하는 '테스트용'으로써의 기능은 정말 좋지만, 이번 미션의 주제가 atdd였기 때문에 mock을 사용하지 않기로 결론을 내렸다. 1단계 요구사항 때 작성해둔 fakeDao 자료구조들도 있고 말이다 ㅎㅎ


Http method는 하나의 약속

RestController를 작성하면서 api를 만들면서 느낀 점이다. 리소스 조회는 GET, 리소스 등록 및 프로세스 처리는 POST, 리소스 대체는 PUT 등등... 하지만 실제로 POST method의 URI 기능으로 리소스 조회 로직을 넣어도 작동은 한다. 그렇다면 왜? http method는 존재하는 것일까?

 

http 공부를 하면서 내린 결론은, http method는 하나의 약속이라는 것이다. 우리가 만든 api를 사용하는 사람들이 이 api를 볼 때 어떻게 설계됐는지 알 수 있도록, 그리고 잘 사용할 수 있도록 정의를 내려주는 것이다. 물론 약속을 지키지 않고 만들어도 기능이 동작되는 데는 전혀 문제가 없다. 하지만 다른 사람들이 우리의 api를 이용하거나 유지보수를 할 때, 해당 api의 명료성과 신뢰성이 떨어져 생산비용이 증가할 것이다.

http method와 URI만으로 해당 기능을 대략적으로 알 수 있다.

따라서 http method를 잘 정의해줄 경우, 타 개발자들이 올바르게 사용해줄 수 있을 뿐 아니라, 우리가 이후에 유지보수를 할 때의 생산비용을 감소시켜줄 수 있다.

 

http method에 대해 공부한 내용을 보고 싶다면 아래 글을 참고하자.

https://kth990303.tistory.com/311

https://kth990303.tistory.com/319

 

[매트 스터디] 2주차 HTTP 기본 & HTTP 메서드

우아한테크코스 레벨2 에서 매트가 주관한 스터디로, 인프런 김영한님의 강의 모든 개발자를 위한 HTTP 웹 기본 지식 스터디를 진행중이다. 이 포스팅에서는 스터디에 PR을 날릴 내용과 함께 스

kth990303.tistory.com


Optional은 조심해서 다루자

Optional은 Java 8에 추가된 키워드이다. 이를 이용하면 특정 메서드의 반환 값이 null이 포함될 때에 보다 똑똑하게 처리할 수 있다. 그렇지만, 옵셔널은 상당히 조심해서 써야 한다. optional이 제공해주는 기능이 생각보다 많아서, 옵셔널을 잘못 사용하면 결국 옵셔널을 사용하지 않는 것과 다름 없는 코드가 완성될 수 있기 때문이다.

 

옵셔널에 대해 좀 더 자세히 알아보자.

1
2
3
4
5
@Override
public Optional<Section> findById(Long id) {
    final String sql = "select * from SECTION where id = ?";
    return Optional.ofNullable(jdbcTemplate.queryForObject(sql, sectionRowMapper, id));
}
cs

이 코드, 만약 null이 담긴다면 어떤 결과를 반환할 것 같은가? Option.empty()? null? 아니다. 이 코드는 null이 담기면 EmptyResultDataAccessException(...?)을 반환한다. 따라서 예외가 발생하게 되고, 이 상황을 예상하지 못했다면 의문사할 수 있는 것이다. 

 

K (나):
Optional.ofNullable일 경우 null을 반환할 수 있지 않나요?
해당 구간이 존재하지 않을 때 반환값으로 null (또는 Optional.empty()) 이 담길 줄 알았는데, 예외가 발생하길래 try-catch문으로 묶어서 처리해주었습니다...

J (리뷰어):
저도 확인해봤는데 DataAccessUtils.nullableSingleResult(results) 에서 비어있는 결과면 예외를 발생시키네요...
의도대로 처리하려면 Exception 캐치가 아닌 EmptyResultDataAccessException 을 잡아야될거같아요.

 

갓제이...

리뷰어님께서 해당 예외에 대해 알려주신 덕분에, 올바르게 예외처리를 할 수 있었다.

 

또한, 옵셔널을 파라미터로 넘겨주는 것은 옵셔널을 적절하게 이용하지 못하고 있을 확률이 높다.

옵셔널은 null 반환 시 오류가 발생될 수 있을 때 결과 없음을 명확하게 드러내기 위해 설계됐기 때문에, 해당 값을 바로 처리하지 않고 파라미터로 직접 넘겨줄 경우, null check 로직 증가 및 위험성 등 단점들이 존재하게 된다.

 

실제로 우테코 4기 크루 연로그도 같은 생각을 했는지 옵셔널을 파라미터로 넘겨주지 말라는 포스팅을 작성했다.

잠시 파워블로거 연로그의 글을 감상해보자.

https://yeonyeon.tistory.com/224

 

[Java] 파라미터를 Optional로 받지 말자

'Optional ' used as type for parameter '파라미터명' 경고 어쩌다보니 Optional 파라미터를 받는 메서드를 만들게 되었다. 노란줄이 쳐지며 경고가 떴다. 대체 왜? 🤔 private void test(Optional id) { // .....

yeonyeon.tistory.com

 

이제 내 코드의 Before-After를 보자.

 

  • Before
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void deleteSection(Long lineId, Long stationId) {
    // ...
    
    final Optional<Section> upSection = sectionDao.findByDownStationId(lineId, stationId);
    final Optional<Section> downSection = sectionDao.findByUpStationId(lineId, stationId);
    checkFinalAndDeleteSections(upSection, downSection);
}
 
// 파라미터로 옵셔널 값을 넘겨주었다.
private void checkFinalAndDeleteSections(Optional<Section> upSection, Optional<Section> downSection) {
    if (upSection.isEmpty() || downSection.isEmpty()) {
        // ...
    }
    // ...
}
cs

 

  • After
1
2
3
4
5
6
7
8
9
10
11
public void deleteSection(Long lineId, Long stationId) {
    // ...
    
    // ifPresent를 이용해주어 파라미터로 옵셔널을 넘겨주지 않도록 해주었다.
    upSection.ifPresent((up -> downSection.ifPresent(down -> updateDownSectionByDeletion(up, down))));
    
    if (upSection.isEmpty() || downSection.isEmpty()) {
        upSection.ifPresent(deleteFinalSection());
        downSection.ifPresent(deleteFinalSection());
    }
}
cs

Optional.ifPresent() 의 괄호 안에는 람다식이 들어가야했기 때문에, 람다식을 넣거나 Consumer를 사용하기도 했다.


DTO에 대한 고찰

내 코드 설계에는 서비스에서 dto <-> domain 변환 작업이 포함돼있다. 컨트롤러에서 요청 dto를 서비스에서 보내주면, 서비스에서는 dto <-> domain 변환작업을 이용하여 도메인 로직/dao 로직을 호출하여 적절한 응답 dto를 반환한다. 이를 컨트롤러에서 받아 api 응답으로 보낸다.

 

dto <-> domain 변환 작업을 컨트롤러에서 할지, 서비스에서 할지 개발자마다 의견이 분분한 것으로 알고 있다. 나는 서비스에서 이 작업을 해주는 편이다. 아래는 내 생각을 정리하면서 리뷰어님과 의견을 공유한 내용이다.

K (나):
현재 저는 Controller에서 요청을 받으면, 그 요청의 dto <-> domain 변환을 서비스에서 해주고 있습니다!
컨트롤러에서는 요청 및 응답 반환 기능에 집중하도록 하고 싶었고, 컨트롤러에서 도메인의 모든 속성을 알거나, 도메인을 의존하는 것이 싫었기 때문이에요. 그렇기 때문에 서비스에서 dto <-> domain 변환 작업을 해주고 있고, 실제로 저는 이 과정을 앞서 말한 이유 때문에 괜찮게 생각하는 편입니다. 그래야 비즈니스 로직에서 도메인 로직을 이용하여 기능을 실행해주고 db 처리도 해줄 수 있다고 생각했기 때문이에요.
추가로, controller와 service에서 같은 dto를 사용하면 서비스에서 컨트롤러에 의존한다는 의견을 내준 크루도 있었습니다! 그렇지만, 컨트롤러와 서비스 dto를 추가로 분리해주면 현재 규모에선 **dto <-> domain 변환작업 로직이랑 클래스가 불필요하게 많아진다 생각하여 컨트롤러용 dto랑 서비스용 dto를 따로 만들진 않았어요! **
제이가 보기에 현재 제 설계는 어떤가요? 추가로, 현재는 dao에서 domain을 반환하는 방향으로 설계했는데 이에 대해서 제이의 생각도 궁금합니다!

J (리뷰어):
저는 말씀주신 이유에 공감하여 현재 구조가 좋은거같아요. 추가로 dto를 분리하는건 완벽한 낭비라고 생각합니다. 추가로 말씀주신 도메인 반환도 저는 알맞게 잘 해주셨다고 생각해요. 구현체에 의존적이지 않으니까요.

 

제이와 나의 생각이 거의 일치했다.

물론 이 부분은 정답이 없는 문제라 생각한다. 또한, 상황에 따라 더 나은 방향이 달라진다고도 생각한다.

나는 이 미션에서 가장 적합하다고 생각한 구조를 작성한 것이고, 다른 크루들이나 개발자가 생각하기에 더 적합한 구조가 존재할 수도 있다. No Silver Bullet – Essence and Accidents of Software Engineering (소프트웨어 공학에 은탄환은 없다)는 말이 괜히 나온 게 아닌 듯하다.


점점 미션들이 재밌어지기도 하지만, 빡세지기도 하는 듯하다.

그만큼 배울 점들도 많아져서 좋긴 하지만, 힘들어지기도 하는 듯하다.

스프링에 대해서 배우면, cs 공부의 필요성도 느끼게 돼서 http, db 공부는 물론이고, 객체지향 공부도 손놓을 수 없다고 생각하기 때문이다.

 

다행히 우테코 크루들과 서로 코딩만 하는 것이 아닌, 대화도 하고 서로 술도 마시면서 으쌰으쌰하기 때문에 계속 달려나갈 수 있는 듯하다. 이제 레벨2가 한 달도 남지 않았는데 열심히 계속 달려봐야겠다.

반응형