4주 차 강의 내용 정리

  • ATDD
  • 인수테스트란
  • 4주 차 미션(Lotto) 대한 피드백 

ATDD(인수테스트 주도 개발)

ATDD 프로젝트를 구성하는 인원들과 원할한 협업을 하기위한 소프트웨어 개발 방법론 하나. 기획 단계부터 공통의 목표와 이해를 도모해서 프로젝트를 진행하기 위해 사용됨.

신규 기능 개발시 발생가능한 문제( 도메인에 대한 이해도 부족으로 단위테스트 작성 어려움, 레거시 리팩터링의 어려움 ) 인해 TDD 사이클로 개발이 어려울 경우 인수테스트를 먼저 구현한 이후 단위 테스트를 통해 기능을 완성해 가는 과정 대안이 있다.

 

ATDD 사이클

ATDD 개발 프로세스 

 

  • 예시 1(신규 기능 개발)

요구사항 분석

 - 인수 테스트가 충족해야하는 조건

 - 분석 및 표현에 다양한 방식이 있다.

Ex)시나리오 기반으로 표현하는 방식 (Given/When/Then)

https://www.altexsoft.com/blog/business/acceptance-criteria-purposes-formats-and-best-practices/

 

인수 테스트

 - 분석한 요구사항을 검증하는 단계

 - 실제 요청/응답하는 환경과 유사하게 테스트 환경을 구성

 

문서화

 - Spring Rest Docs를 활용한 API 문서화

 - 문서화를 위해서는 Mock 서버 & DTO 정의가 필요.

  1. 프론트엔드, 다른 백엔드 개발자와 병렬로 작업하는데 유리함
  2. 인수 테스트와는 별개로 API 테스트를 수행
  3. 다른 개발자들과 협업 시 커뮤니케이션에 큰 장점

기능 구현 with TDD

 - 인수테스트 기반으로 하나씩 기능개발을 진행

 - Outside In / Inside Out 방향으로 개발

 

이후 반복적인 리펙터링

 

  • 예시 2(레거시 코드 리펙터링)

리팩터링 대상을 확인

 

리팩터링 방법 선택

 - 먼저 인수 테스트를 작성하여 기존에 구현된 기능을 보호하기

 - 파악이 가능한 부분 부터 단위 테스트를 만들어 기능 검증하기

 

인수 테스트의 보호 속에서 리팩터링 하자!

 

새로운 인수테스트를 작성

 

메서드 분리

 - 인수 테스트 작성 후 메서드 분리 시 사이드 이펙트에 대한 피드백을 바로 받을 수 있음

 - 기능을 구현하다 꼬이거나 잘못 되어도 인수 테스트가 성공한 시점으로 리셋할 수 있음

 - 안심하고 작은 단위로 메서드를 분리

 

단위 테스트 작성

 - 단위가 작아지니 역할이 줄어들고 검증할 대상이 명확해 짐

 - 단위 테스트 하기 쉬운 코드가


인수테스트란

실제 사용자 환경에서 사용자의 입장으로 테스트를 수행하는 것을 말한다. 인수 조건은 개발 용어가 사용되지 않고 일반 사용자들이 이해할 있는 단어를 사용

 

 

인수 테스트 특징

 

전구간 테스트

  • 요청과 응답 기준으로 전 구간을 검증

Black Box 테스트

  • 세부 구현에 영향을 받지 않게 구현하기

 

테스트 만드는 과정

 

테스트 환경 구축

인수 테스트는 시스템 내부 코드를 가능한 직접 호출하지 말고 시스템 구간을 테스트를 하도록 안내하고 있기 때문에 시스템 외부에서 요청하는 방식으로 검증함.

 

인수 테스트 클래스

  • 실제 서버가 아닌 테스트를 위한 서버를 로드하도록 설정
  • 실제 request가 아닌 인수 테스트의 request를 받기 위함
  • @SpringBootTest을 클래스에 붙여주면 테스트를 위한 웹 서버를 사용
  • webEnvironment 설정을 통해 서버의 환경을 지정

 

인수 테스트 객체 설정

  • 테스트를 위한 서버에 요청을 보내기 위한 클라이언트 객체 설정
    • ex. MockMvc, RestAssured, WebTestClient
    • 테스트의 성격에 맞는 클라이언트를 선택해야
MockMvc vs WebTestClient vs RestAssured=> 비교는 다음 포스팅 예정

테스트 서버에 요청 / 응답 결과를 검증

ex)

@DisplayName("지하철역을 생성한다.")
@Test
void createStation() {

    ...
       
    ExtractableResponse<Response> response = RestAssured.given().log().all()
            .body(params)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .when()
            .post("/stations")
            .then().log().all()
            .extract();

    // then
    assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value());
    assertThat(response.header("Location")).isNotBlank();
}

@SpringBootTest

  • 테스트에 사용할 ApplicationContext를 쉽게 지정하게 도와줌
  • 기존 @ContextConfiguration의 발전된 기능
  • SpringApplication에서 사용하는 ApplicationContext를 생성해서 작동

 

webEnvironment

@SpringBootTest의 webEnvironment 속성을 사용하여 테스트 서버의 실행 방법을 설정 링크.

  • MOCK: Mocking된 웹 환경을 제공, MockMvc를 사용한 테스트를 진행할 수 있음
  • RANDOM_PORT: 실제 웹 환경을 구성
  • DEFINED_PORT: 실제 웹 환경을 구성, 지정한 포트를 listen
  • NONE: 아무런 환경을 구성하지 않음

 

❗️인수 테스트 작성 팁

인수 테스트 클래스

  • Feature 기준으로 인수 테스트 클래스를 나눌 수 있음
  • Scenario 기준으로 인수 테스트 메서드를 작성할 수 있음
  • 하나의 Feature 내부에 있는 Scenario는 같은 테스트 픽스쳐를 공유하는 것을 추천

간단한 성공 케이스 우선 작성

  • 동작 가능한 가장 간단한 성공 케이스로 시작
  • 테스트가 동작하면 실제 구조에 관해 더 좋은 생각이 떠오를 수 있음
  • 그 과정에서 발생 가능한 실패를 처리하는것과 성공 케이스 사이에서 우선순위를 가늠

실패하는 테스트 지켜보기

  • 코드를 작성하기 전 테스트가 실패하는 것을 지켜본 후 진단 메시지를 확인
  • 테스트가 예상과 다른 식으로 실패하면 뭔가를 잘못 이해했거나 코드가 완성되지 않았다는 뜻

Given / When / Then

  • When -> Then -> Given 순서로 작성하는 것이 자연스러움

🧑‍💻 인수 테스트 리팩토링

 

테스트의 의도를 명확히 드러내기

가독성에 신경쓰기!

  • 가독성이 좋지 않으면 방치되는 테스트가 될 가능성이 높다
  • 변경 사항에 대해서 수정하기 어렵다. -> 방치될 가능성 높다
    • feat. @Ignore or @Disabled
  • 가독성이 좋으면 해당 기능의 스펙을 나타낼 수 있다.

프로덕션 코드의 가독성이 중요한 만큼 테스트 코드의 가독성도 중요함

 

테스트 코드 중복 제거

  • 기능 개발 간 테스트 코드도 중복이 많이 발생함
  • 테스트 가독성을 저해하는 구조가 나올 수 있어 중복제거가 중요함
  • 가독성이 좋지 않은 테스트 코드는 관리가 되지 않는 가능성이 높음

중복 제거 방법

  • 메서드 분리
  • CRUD 추상화
  • Cucumber JBehave 같은 BDD 도구 사용

메서드 분리

  • 반복되는 코드는 메서드로 분리
  • ex) StationAcceptanceTest

다른 인수 테스트에서 재사용

  • 스텝 메서드들을 static 선언
  • 다른 인수 테스트에서 재사용 가능

4주 차 미션 (JPA)

  • JPA 실습 진행. 관련 문법 학습

소스코드: github.com/louisJu/jwp-jpa-ex

느낀 점

  • 김영한님의 JPA 참고서적을 학습하면서 해당 미션을 진행하니 수월하게 느껴졌다.
  • ENTITY들의 관계성을 잘 고려하면서 설계하는것은 어렵구나 다시한번 느낌.

회고 

 : 생각보다 어려운 피드백없이 진행했던 과제. 하지만 JPA는 너무나도 방대하다는 것을 앎.

 


참고자료

우아한테크캠프 4주차 강의.

ATDD 요구사항 분석 방법:

https://www.altexsoft.com/blog/business/acceptance-criteria-purposes-formats-and-best-practices/

 

ATDD방법론 설명:

https://mysoftwarequality.wordpress.com/2013/11/12/when-something-works-share-it/

3주 차 강의 내용 정리

  • JPA
  • 3주 차 미션(Lotto) 대한 피드백 

JPA

SQL을 직접 다룰 때 발생하는 문제점

  1. 반복 작업
    테이블 구조가 변경(추가, 삭제)되면 관련된 SQL을 전부 수정해줘야 한다.
  2. 신뢰성
    Human error가 발생할 가능성이 높아짐 (ex. 데이터 fetch 로직에서 테이블 간 매핑이나 쿼리 작성이 빠져있는 경우)

=> JPA를 사용하면 위 issue에서 자유로워질 수 있다.

 

더보기

JDBC, SQLMAPPER, ORM

공통점: persistence (데이터를 영속적으로 저장하기 위해 사용하는 기술) 

 

JDBC: java의 JDBC API

          DriverManager -> Connection -> Statement -> ResultSet 으로 이루어져 있는 구조

          커넥션, 쿼리를 직접 작성하고 관리해야 한다.

 

SQL MAPPER: MyBatis, Spring JDBC

                       SQL을 자바 코드로부터 분리한 것

 

ORM: JPA, Hibernate, Spring data JPA, Sping data JDBC

          사람이 쿼리를 관리하지 않는 것! (Lazy Loading, Dirty Checking, Caching 등의 특징이 있음)

 

 

JPA 특징

  1. 데이터베이스 스키마를 자동으로 생성하는 기능을 지원

spring.jpa.hibernate.ddl-auto=create
더보기

설정값 종류

create: 기존 테이블 삭제 후 다시 생성 (DROP + CREATE)

create-drop: create와 같으나 종료 시점에 테이블 DROP

update: 변경된 부분만 반영 (운영 DB에 사용하면 안됨)

validate: entity와 table이 정상 매핑되었는지만 확인

none: 사용하지 않음

 

※ 크리티컬 이슈를 발생시킬 수 있기 때문에 실무에서는 보통 validate or none으로만 사용한다. 

 

    2. @(Annotation)으로 많은 기능을 활용할 수 있다.

@Entity // (1)
@Table(name = "station") // (2)
public class Station {
    @Id // (3)
    @GeneratedValue(strategy = GenerationType.IDENTITY) // (4)
    private Long id;

    @Column(name = "name", nullable = false) // (5)
    private String name;
    
    protected Station() { // (6)
    }
}
  1. @Entity : pk 꼭 가져야함.  엔티티 클래스임을 지정하며 테이블과 매핑된다.
  2. @Table: 굳이 선언할 필요 없음. Name property 이용해서 컨벤션에 맞는 테이블이름을 정할 있다. 생략할 경우 엔티티클래스이름과 동일한 테이블로 매핑
  3. @Id : pk
  4. @GeneratedVAlue : pk 생성 규칙을
  5. @column
    • 컬럼의 이름을 이용하여 지정된 필드나 속성을 테이블의 컬럼에 매핑한다.
    • 굳이 선언하지 않아도 된다.
  6. 매개 변수가 없는 생성자
    • The entity class must have a no-arg constructor. The entity class may have other constructors as well. - JSR 338

   3. 영속성 컨텍스트

  • 엔티티를 영구 저장하는 환경
  • 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다
  • 영속성 컨텍스트의 기능
    - 1차 캐시
    - 동일성 보장
    - 트랜잭션을 지원하는 쓰기 지연
    - 변경 감지
    - 지연 로딩

  4. 엔티티의 생명주기

비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
영속(managed): 영속성 컨텍스트에 저장된 상태
준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
삭제(removed): 삭제된 상태

 

JPA의 연관관계 

엔티티들은 대부분 다른 엔티티와 연관 관계를 맺는다.

객체는 참조를 사용, 테이블은 외래 키를 사용해서 관계를 맺는다.

  • 방향: 단방향, 양방향이 있다. 방향은 객체 관계에서만 존재하고 테이블 관계는 항상 양방향이다.
  • 다중성: 일대일, 다대다, 일대다, 다대일 
  • 연관관계의 주인
    • 객체 사이에 관계가 형성되면 관계의 주인을 정해야 한다.
    • 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 등록, 수정, 삭제할 수 있다.
    • 주인이 아닌 쪽은 읽기만 가능하다.
  • 연관관계의 주인이 아닌 곳에서 입력된 값은 외래 키에 영향을 주지 않는다.
  • 양방향 관계에서는 양쪽 다 데이터 일관성에 대해 신경을 써야 한다.
  • 양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전하다.
  • 한 번에 양방향 관계를 설정하는 메서드를 연관 관계 편의 메서드라 한다.
public void setLine(Line line) {
    this.line = line;
    line.getStations().add(this);
}

public void addStation(Station station) {
    stations.add(station);
    station.setLine(this);
}
매핑 시 무한루프에 빠지지 않게 조심해야 한다.

 

TIPS

  • AUDITING
    애플리케이션의엔티티의 생성 시간과 마지막 수정시간을 관리할 필요가 있다면 수동으로 매번 추가하는 대신 Auditing 기능을 이용해서 자동으로 추가해 있다.

  1. @Configuration 클래스에 @EnableJpaAuditing 추가한다.
@EnableJpaAuditing
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

    2. 엔티티에 콜백 리스너를 추가한다.

@EntityListeners(AuditingEntityListener.class)
@Entity
public class Line {

    3. 생성 날짜와 마지막 수정 날짜 프로퍼티에 @CreatedDate @LastModifiedDate 추가한다.

@EntityListeners(AuditingEntityListener.class)
@Entity
public class Line {
    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;

    4. @MappedSuperclass 를 사용해서 abstract class 생성해서 사용한다면 중복코드를 분리할 있다.

  • 비교
    엔티티 비교시에는 식별자있어도 충분하다.

좀 더 디테일한 JPA 관련 내용은 추후 해당 블로그에 기재하도록 하겠다.

 

3주 차 미션에 대한 피드백

  • . 매직 넘버는 명명된 상수로 치환해서 사용하기
// AS-IS
public class Calculator {

    public int splitAndSum(String text) {
        String[] numArr = Splitter.split(!isEmptyOrNull(text) ? text : "0");

//TO-BE
public class Calculator {
	private final static String ZERO = "0";

    public int splitAndSum(String text) {
        String[] numArr = Splitter.split(!isEmptyOrNull(text) ? text : ZERO);
        

 

  • 유틸리티 클래스의 접근성 제한하기
    • 유틸 클래스의 경우 불필요한 객체 생성을 방지하기 위해서 Private 생성자를 사용하기
  • 직관적인 코드 작성
//AS-IS
if(inputValue == null || inputValue.isEmpty()) {
	return true;
}
	return false;
        
//TO-BE       
 return inputValue == null || inputValue.isEmpty();

 

  • 스트림을 사용해서 간략하게 코드 작성
//AS-IS
int resultNum = Integer.parseInt(numArr[0]);
for(int i = 1; i < numArr.length; i++) {
	resultNum = add(resultNum, Integer.parseInt(numArr[i]));
}

	return resultNum;

//TO-BE
return Arrays.stream(numArr)
.mapToInt(Integer::parseInt)
.sum();

 

  • RetainAll 함수를 사용하는 방법
//AS-IS
return lotto.nums.stream().
	filter(n -> this.nums.contains(n)).count();
    

 

  • 로또 2,3등 구하는 로직 개선
//AS-IS

    public static Rank of(int matchNums, boolean matchBonus) {
        if (matchNums == SECOND.matchNums && matchBonus) {
            return SECOND;
        }
        if (matchNums == THIRD.matchNums) {
            return THIRD;
        }
        return Arrays.stream(Rank.values())
                .filter(rank -> rank.matchNums == matchNums)
                .findAny()
                .orElse(MISS);
    }
    
//TO-BE

    return Arrays.stream(values())
        .filter(rank -> rank. matchNums == matchNums)
        .filter(rank -> !rank.equals(SECOND) || matchBonus)
        .findFirst()
        .orElse(MISS);

3주 차 미션(Lotto) 리뷰

소스코드: github.com/louisJu/java-lotto-ex

느낀 점

  • 상수처리에 대해서 조금 더 신경 써야 할 것 같음.
  • ENUM 클래스 활용에 아직 많이 미숙하구나. 
  • 스트림 활용법에 대한 학습 필요.

회고 

 : 아직까진 예전에 실습해본 주제로 과제를 진행하다 보니 접근하기 나름 편했다. 역시 반복학습의 효과는 짱...

2주 차 강의 내용 정리

  • TDD, 클린코드
  • 1주차미션(racingCar) 대한 피드백 & 라이브 코딩

Clean Code 가이드

의미 있는 이름

  • 의도를 분명히 한 naming 규칙
    • 협업할 때 새롭게 코드를 볼 사람을 생각해서 더 나은 이름으로 작성하기
    • 주석을 사용하지 않아도 될 정도로 분명하게 naming 하기
// 잘못된 변수 사용
int d; // 경과시간(day)
  • 의미 있게 구분하기
    • 기준을 세우고 네이밍하는 것이 좋다. (ex. 카멜케이스, 스네이크케이스 등)
  • 인터페이스 클래스와 구현 클래스
    • 인터페이스는 공통적인 개념으로 naming하고, 구현 클래스는 의도가 드러날 수 있는 구체적인 naming을 사용하라
  • 클래스 이름
    • 명사, 명사구가 적합
    • Manager, Processer, Data, Info 등과 같은 단어는 피하고, 동사는 사용하지 않는다.
  • 메소드 이름
    • 동사, 동사구가 적합
    • 접근자, 변경자, 조건자에는 값 앞에 get, set, is를 붙인다.
    • 생성자를 중복해서 정의할 땐 정적팩토리 메소드를 사용. 메소드명은 인수를 설명하는 이름으로 사용한다.

경계

wrapper 클래스를 사용

Map, List 같은 collection 등 외부에 노출하는 경우 많은 인터페이스가 노출하게 되기 때문에 wrapper 클래스를 사용해서 숨기는 것이 좋다.

  • 사용자에게 모든 인터페이스를 노출하지 않아도 된다.
  • 내부적인 변경이 발생하더라도 외부 변경은 발생하지 않는다.
Map<Integer, Sensor> sensors = new HashMap<>();
Sensor s = sensors.get(sensorId);
public class Sensors {
    Map<Integer, Sensor> sensors = new HashMap<>();

    pubilc Sensor getById(String id) {
        return sensors.get(id);
    }
}

=> Map을 사용자에게 직접 노출하지 않고 Sensors 클래스르 이용해서 처리 가능하다.

1주차 미션에 대한 피드백

  • 로직이 동일한 테스트일 경우 @ParameterizedTest 를 활용해서 하나로 합칠 수 있다. 
    @ParameterizedTest
    @CsvSource({"20 + 10,30","20 - 10,10","20 * 10,200","20 / 10,2"})
    @DisplayName("사칙연산 test")
    void calculationTest(String inputData, Long result) {
        expression = new Expression(inputData);
        calculator = new Calculator(expression);
        Long testResult = calculator.calculate();
        assertThat(testResult).isEqualTo(result);
    }

 

 

  • 함수형 인터페이스 및 람다 사용.
//AS-IS
private static List<Car> mapCars(int carNums) {
        List<Car> cars = new ArrayList<>();
        for (int i = 0; i < carNums; i++) {
            cars.add(new Car());
        }
        return cars;
    }
//TO-BE
this.cars = Arrays.stream(splitCarNames(carNames)).map(name -> new Car(name)).collect(Collectors.toList());

 

  • 전략패턴 사용
    => 자동차가 움직이는 규칙을 독립시킬 수 있다. 규칙이 독립되면 테스트도 용의해진다. 
public interface MoveStrategy {
    boolean isMove(int value);
}

=> 이동 규칙 관련 인터페이스를 생성

 

public class RandomMoveStrategy implements MoveStrategy {
    private static final int START_CONDITION = 4;
    private static final int END_CONDITION = 9;

    @Override
    public boolean isMove(int value) {
        if(value >= START_CONDITION && value <= END_CONDITION) {
            return true;
        }
        return false;
    }
}

=> 인터페이스를 구현한 실제 적용할 규칙 클래스를 생성

 

public class Racing {
    private List<Car> cars;
    private MoveStrategy moveStrategy;

    public Racing(String carNames) {
        this.cars = Arrays.stream(splitCarNames(carNames)).map(name -> new Car(name)).collect(Collectors.toList());
        this.moveStrategy = new RandomMoveStrategy();
    }

    public void race() {
        for (Car car : cars) {
            car.racing(RacingCarUtils.randomValueGenerator(), moveStrategy);
        }
    }
}

=> 로직에 적용

 

2주차미션(racing-car) 리뷰

소스코드: github.com/louisJu/java-racingcar-ex

 

느낀 점

  • 변수명 결정하는데 시간이 오래 걸렸음. 위에서 언급한 사이트들을 이용한다면 조금 빠르게 naming 을 할 수 있지 않을까?
  • 전략패턴을 학습 및 적용해보면서 코드가 좀 더 이뻐지는 느낌을 받음
  • 테스트 코드 작성에도 신경을 많이 써야할 것 같음
  • 스트림, 람다를 의식적으로 많이 쓰도록 하자!

회고 

 : 코딩을 하다보면 나도 모르게 익숙하고 편한 방법(레거시를 만들어내는) 하게 된다. 의식적인 코딩을 통해서 좀더 나은 코딩을 하도록 노력하자. 
: 이유없는 코드라인은 만들지 않도록 하자.

+ Recent posts