[Spring] 구인구직 서비스3 - 개발자 API 구현

춤인형의 개발일지·2025년 1월 31일

Spring실습

목록 보기
24/40

구인구직 서비스

수요일 - API 구현

개발자

개발자부분을 먼저 개발해보았다. 늘 하던대로 controller를 첫번째로 만들었다. api spec을 보고 만드는거라 크게 어렵지 않았다.

개발자 Controller

@RestController
public class ProgrammerRestController {

    private final ProgrammerService programmerService;

    public ProgrammerRestController(ProgrammerService programmerService) {
        this.programmerService = programmerService;
    }

    //생성
    @PostMapping("/programmers")
    public ProgrammerResponse create(@RequestBody ProgrammerCreateRequest programmerRequest){
        return programmerService.create(programmerRequest);
    }

    //간단한 조회
    @GetMapping("/programmers")
    public List<ProgrammerReadResponse> findAll(@LoginMember String authorization,
                                                @RequestParam List<Field> field,
                                                @RequestParam Integer personalHistory){
        return programmerService.findAll(authorization,field,personalHistory);
    }

    //상세 조회
    @GetMapping("/programmers/{programmerId}")
    public ProgrammerDetailResponse findById (@LoginMember String authorization,
                                              @PathVariable String programmerId){
        return programmerService.findById(authorization, programmerId);
    }

    //상세 조회 - 내 정보
    @GetMapping("/programmers/my")
    public ProgrammerResponse findByMypage (@LoginMember String authorization){
        return programmerService.findByMyPage(authorization);
    }

    //내 정보 수정
    @PutMapping("/programmers/my")
    public ProgrammerResponse updateMypage (@RequestBody ProgrammerRequest programmerRequest,
                                            @LoginMember String authorization){
        return programmerService.updateMyPage(programmerRequest, authorization);
    }

    @PatchMapping("/programmers/my")
    public void updatePassword(@LoginMember String authorization,
                               @RequestBody ProgrammerPasswordRequest programmerPasswordRequest){
        programmerService.updatePassword(authorization, programmerPasswordRequest);
    }

    //내 정보 삭제
    @DeleteMapping("/programmers/my")
    public void deleteMypage(@LoginMember String authorization){
        programmerService.deleteById(authorization);
    }

개발자 service

service, repository를 그 다음에 만들었다.
service를 만들면서 필요한 request들을 함께 만들었다.

1. 개발자 생성

  • programmerRequest에는 userId,password,name, age,email,personalHistory,fieldName,selfIntroduction,certificate를 받고,
    ProgrammerResponse에는 password만 뺀 나머지 데이터들이 잘들어왔는지 확인해줘야한다.
    public ProgrammerResponse create(ProgrammerCreateRequest programmerRequest) {
        Programmer programmer = programmerRepository.save(new Programmer(
                programmerRequest.userId(),
                programmerRequest.password(),
                programmerRequest.name(),
                programmerRequest.age(),
                programmerRequest.email(),
                programmerRequest.personalHistory(),
                programmerRequest.fieldName(),
                programmerRequest.selfIntroduction(),
                programmerRequest.certificate()));

        return new ProgrammerResponse(
                programmer.getId(),
                programmer.getUserId(),
                programmerRequest.name(),
                programmerRequest.age(),
                programmerRequest.email(),
                programmerRequest.personalHistory(),
                programmerRequest.fieldName(),
                programmerRequest.selfIntroduction(),
                programmerRequest.certificate());
    }

2. 조회

  • 조회는 정렬이 들어간다.
    좋아요를 기본 정렬로 해놓고, 나머지는 정렬이 들어오면 그 부분을 처리하게 된다. 이 부분을 원래 하던 방식인 query method로 해주면 너무 길어지고, 이상해지기 때문에 이런 문제를 해결해주기 위해서는 querydsl을 사용해야한다. 이 부분은 querydsl에서 다시 다뤄보도록 하자.

    ❓왜 QueryDsl을 사용할까?

    • JPA Query Methods를 사용하면 점점 메서드 이름이 지나치게 길어져서 가독성 떨어짐
    • JPA Query Methods와 JPQL은 실행 전(컴파일 타임)에
      기본적인 오류조차 확인하기 힘듦 (❌ Type-safe)

3. 개발자 상세조회

  • 개발자의 기본 id를 찾은 다음,
    ProgrammerDetailResponse의 로그인할 때 처리하는 id, password를 뺀 나머지 데이터를 빼온다.
public ProgrammerDetailResponse findById(String authorization, String programmerId) {
        Programmer programmer = programmerRepository.findById(programmerId)
                .orElseThrow(() -> new NoSuchElementException("찾으시는 개발자가 없습니다."));

        return new ProgrammerDetailResponse(
                programmer.getName(),
                programmer.getAge(),
                programmer.getEmail(),
                programmer.getPersonalHistory(),
                programmer.getFieldName(),
                programmer.getSelfIntroduction(),
                programmer.getCertificate(),
                isLiked(authorization, programmerId),
                programmer.getLikeCount());
    }

4. 개발자 정보(자기자신을 수정하는 용)를 조회할 때

근데 이게 왜 필요한지 지금도 이해가 안간다.. 자기 정보를 조회하는건 왜? 그냥 자기 정보를 수정할 때만 있어도 되는거 아닌가?
-> 왜냐하면 자기정보를 수정할 때만 보는건 기본적으로 말이 안되지 않는가? 따라서 기본적으로 조회하는 것도 필요하다.

  • password제외한 나머지 데이터를 불러온다.
    public ProgrammerResponse findByMyPage(String authorization) {
        Programmer programmer = programmerRepository.findById(authorization)
                .orElseThrow(() -> new NoSuchElementException("로그인 정보가 없습니다."));

        return new ProgrammerResponse(
                programmer.getId(),
                programmer.getUserId(),
                programmer.getName(),
                programmer.getAge(),
                programmer.getEmail(),
                programmer.getPersonalHistory(),
                programmer.getFieldName(),
                programmer.getSelfIntroduction(),
                programmer.getCertificate());
    }

5.정보 수정

  • password를 제외한 나머지를 수정한다.

password는 수정이 아니라, 보통 재설정하기 때문에 비밀번호는 제외하고 만든다.

❓왜 paaword를 재설정하냐면,
비밀번호는 현재 해쉬화가 되어 있다. 따라서 서버에 저장된 비밀번호를 찾아주는 방식을 사용하면 해커가 해킹할 가능성이 크다. (해커들은 똑똑해서,,)해쉬화된 비밀번호를 복호화를 할 가능성이 크기 때문에 조심해야한다.

    @Transactional
    public ProgrammerResponse updateMyPage(ProgrammerRequest programmerRequest, String authorization) {
        Programmer programmer = programmerRepository.findById(authorization)
                .orElseThrow(() -> new NoSuchElementException("로그인 정보가 없습니다."));

        programmer.setUserId(programmerRequest.userId());
        programmer.setName(programmerRequest.name());
        programmer.setAge(programmerRequest.age());
        programmer.setEmail(programmerRequest.email());
        programmer.setPersonalHistory(programmerRequest.personalHistory());
        programmer.setFieldName(programmerRequest.fieldName());
        programmer.setSelfIntroduction(programmerRequest.selfIntroduction());
        programmer.setCertificate(programmerRequest.certificate());

        return new ProgrammerResponse(
                programmer.getId(),
                programmer.getUserId(),
                programmer.getName(),
                programmer.getAge(),
                programmer.getEmail(),
                programmer.getPersonalHistory(),
                programmer.getFieldName(),
                programmer.getSelfIntroduction(),
                programmer.getCertificate());
    }

6. 비밀번호 변경

  • 현재 이 방식은 비밀번호 변경에만 해당한다. 고칠 필요가 있다.
  1. 로그인한 상태에서 본인 인증 후 비밀번호를 바꾸는 것
  2. 토큰(authorization)을 활용해서 사용자를 확인하는 방식
    이런 방식을 현재 사용하고 있는데 다시 수정해야한다.
    @Transactional
    public void updatePassword(String authorization, ProgrammerPasswordRequest programmerPasswordRequest) {
        Programmer programmer = programmerRepository.findById(authorization)
                .orElseThrow(() -> new NoSuchElementException("로그인 정보가 없습니다."));

        programmer.setPassword(programmerPasswordRequest.password());
    }

7. 회원 탈퇴

  • 로그인한 회원이 있는지 없는지 토큰으로 확인해서 있으면 지운다.
    보통은 softdelete를 활용한다고 하는데, 일단 사용하는 방법을 모르니까 그냥 해본다.
    public void deleteById(String authorization) {
        programmerRepository.findById(authorization)
                .orElseThrow(() -> new NoSuchElementException("로그인 정보가 없습니다."));

        programmerRepository.deleteById(authorization);
    }

8. 좋아요를 누른 대상

좋아요를 눌렀는지 안눌렀는지 true, false로 판별해준다.

    private Boolean isLiked(String senderId, String receiverId) {
        return likeRepository.findBySenderIdAndReceiverId(senderId, receiverId) != null;
    }
}

Querydsl

  • querydsl은 repository를 만들어주고, 똑같이 사용해주면 된다.
@Repository
public class ProgrammerQueryRepository {

    private final JPAQueryFactory jpaQueryFactory;
    private final QProgrammer programmer = QProgrammer.programmer;

    public ProgrammerQueryRepository(JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

최초 화면은 좋아요순 정렬이다.
좋아요순은 내림파차이니까 .desc()을 붙여야된다.
분야 선택, 경력 선택 2가지 선택지가 있다. 따라서 .where절로 조건을 걸어둔다.
최종적으로 필터링된 데이터를 리스트 형태로 가져옴(.fetch)

 public List<Programmer> findAll(
            List<Field> fieldNames,  // 선택된 분야들
            Integer personalHistory   // 선택된 경력
    ) {
        return jpaQueryFactory
                .selectFrom(programmer)
                .where(
                        // 분야 여러개 선택 필터링
                        fieldNameCriteria(fieldNames),

                        // 경력 필터링
                        personalHistoryCriteria(personalHistory)
                )
                // 좋아요 순 정렬 (내림차순)
                .orderBy(programmer.likeCount.desc())
                .fetch();
    }

분야는 여러개를 선택할 수 있기 때문에 그것도 고려를 해야한다.

    // 분야 다중 선택 메서드
    private BooleanExpression fieldNameCriteria(List<Field> fieldNames) {
        if (fieldNames == null || fieldNames.isEmpty()) {
           return null;
        }
        return programmer.fieldName.in(fieldNames);
    }

.in
programmer.fieldName에 fieldNames 리스트에 포함된 데이터만 조회

    // 경력 필터링 메서드
    private BooleanExpression personalHistoryCriteria(Integer personalHistory) {
        if (personalHistory == null) {
            return null;
        }
        return programmer.personalHistory.goe(personalHistory);
    }
}

goe()는 >= 연산자
즉, personalHistory가 3이면 경력이 3년 이상인 프로그래머만 조회
해서 개발자 조회의 querydsl를 만들었다.

public List<ProgrammerReadResponse> findAll(String authorization, List<Field> field, Integer personalHistory) {

        List<Programmer> programmerList = programmerQueryRepository.findAll(field, personalHistory);
        List<ProgrammerReadResponse> programmerReadResponses = new ArrayList<>();

        for (Programmer p : programmerList) {
            programmerReadResponses.add(
                    new ProgrammerReadResponse(
                            p.getId(),
                            p.getName(),
                            p.getAge(),
                            p.getFieldName(),
                            isLiked(authorization, p.getId()),
                            p.getLikeCount()));

        }

        return programmerReadResponses;
    }

그래서 이렇게 쿼리리포지토리를 가져와서 사용하게 된다.

나머지 개발자repository /

public interface ProgrammerRepository extends JpaRepository<Programmer, String> {
    Programmer findByUserId(String s);
}

지금까지 짠 부분을 테스트해야한다.


테스트
문제는 테스트였다.

1. 테스트 분리

a. 테스트와 local에 있는 데이터를 분리를 해줘야한다. 그래서 다 분리를 해주면 서로 신경을 안쓰니까 좋다.

  • application.properties
    • 기본 설정들
  • application-local.properties
    • spring.datasource.url
    • spring.datasource.username / password
  • application-test.properties
    • 아무것도 없어도 됨

b. 테스트끼리도 영향을 받지 않았으면 좋겠어서 테스트 격리를 해주었다.
데이터가 들어있으면 테스트끼리도 서로서로 영향을 받기 때문에 격리가 필요하다.

@Service
public class DatabaseCleanup implements InitializingBean {
    @PersistenceContext
    private EntityManager entityManager;
    private List<String> tableNames;
    @Override
    public void afterPropertiesSet() {
        tableNames = entityManager.getMetamodel().getEntities().stream()
                .filter(e -> e.getJavaType().getAnnotation(Entity.class) != null)
                .map(e -> e.getName()
                        .replaceAll("([a-z])([A-Z])", "$1_$2") // camel case to snake case
                        .toLowerCase())
                .collect(Collectors.toList());
    }
    @Transactional
    public void execute() {
        entityManager.flush();
        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();
        for (String tableName : tableNames) {
            entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
            //밑에꺼를 주의
            entityManager.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN ID RESTART WITH 1").executeUpdate();
        }
        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
    }

이렇게 테스트 격리 코드를 넣으니까 아래와 같은 에러가 났다.

Failed to load ApplicationContext for 
[WebMergedContextConfiguration@5b55c3d6 testClass = ~~

이렇게 알 수 없는 에러가 났을 땐, 테스트 실행이 아닌 그냥 웹 실행을 해보면 답이 나온다. 거기서 나온 에러문은 ALTER를 사용할 수 없다는 것이다. postsql에서는 alter을 사용할 수 없다 한건가,, 여튼 alter문을 사용할 수 없어서 에러가 났다. 그래서 주석처리해서 사용했더니 테스트가 돌아갔다.

  • 개발자 생성 테스트
@ActiveProfiles("test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class programmerTest {

    @LocalServerPort
    int port;
    
    @Autowired
    DatabaseCleanup databaseCleanup;
    
    @Autowired
    JwtProvider jwtProvider;
    
    @BeforeEach
    void setUp() {
        databaseCleanup.execute();
        RestAssured.port = port;
    }
    
    @Test
    void 개발자생성() {
        ProgrammerResponse 개발자 = RestAssured
                .given().log().all()
                .contentType(ContentType.JSON)
                .body(new ProgrammerCreateRequest(
                        "userId",
                        "abc123!",
                        "chu",
                        24,
                        "email",
                        1,
                        "백엔드",
                        "안녕하세요",
                        "없음"))
                .when()
                .post("/programmers")
                .then().log().all()
                .statusCode(200)
                .extract()
                .as(ProgrammerResponse.class);
        assertThat(개발자).isNotNull();
    }
  • 개발자 상세조회 테스트
    개발자를 만들고, 다 잘 들어갔는지 확인해야한다.
@Test
    void 개발자_상세조회() {
        ProgrammerResponse 개발자 = RestAssured
                .given().log().all()
                .contentType(ContentType.JSON)
                .body(new ProgrammerCreateRequest(
                        "userId",
                        "abc123!",
                        "chu",
                        24,
                        "email",
                        1,
                        "백엔드",
                        "안녕하세요",
                        "없음"))
                .when()
                .post("/programmers")
                .then().log().all()
                .statusCode(200)
                .extract()
                .as(ProgrammerResponse.class);
                
        ProgrammerDetailResponse 개발자상세 = RestAssured
                .given().log().all()
                .contentType(ContentType.JSON)
                .pathParam("programmerId", 개발자.id())
                .when()
                .get("/programmers/{programmerId}")
                .then().log().all()
                .statusCode(200)
                .extract()
                .as(ProgrammerDetailResponse.class);
        assertThat(개발자상세.name()).isNotNull();
        assertThat(개발자상세.age()).isNotNull();
        assertThat(개발자상세.email()).isNotNull();
        assertThat(개발자상세.personalHistory()).isNotNull();
        assertThat(개발자상세.fieldName()).isNotNull();
        assertThat(개발자상세.selfIntroduction()).isNotNull();
        assertThat(개발자상세.certificate()).isNotNull();
    }
  • 개발자 상세조회(내정보-수정용)
  1. 개발자 만들고
  2. 로그인 해서
  3. 조회한다.

현재 로그인 토큰 생성하는 부분이 없으니까, 어떻게 해결할까 하다가 본게 대충 토큰 같이 생긴걸 넣어준다. 토큰은 string값이니까 string값 아무거나 넣어준다.

@Test
    void 개발자_내정보_상세조회() {
        ProgrammerResponse 개발자 = RestAssured
                .given().log().all()
                .contentType(ContentType.JSON)
                .body(new ProgrammerCreateRequest(
                        "userId",
                        "abc123!",
                        "chu",
                        24,
                        "email",
                        1,
                        "백엔드",
                        "안녕하세요",
                        "없음"))
                .when()
                .post("/programmers")
                .then().log().all()
                .statusCode(200)
                .extract()
                .as(ProgrammerResponse.class);
                
        //로그인
        String token = jwtProvider.createToken(개발자.id());
        
        ProgrammerResponse 개발자상세 = RestAssured
                .given().log().all()
                // TODO: "token" 실제 코드 작성
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                .when()
                .get("/programmers/my")
                .then().log().all()
                .statusCode(200)
                .extract()
                .as(ProgrammerResponse.class);
    }
  • 개발자 수정테스트
  1. 개발자 만들고
  2. 로그인하고
  3. 수정해서
  4. 제대로 들어갔나 확인
@Test
    void 개발자_수정() {
        String 수정전이름 = "수정 전 이름";
        String 수정후이름 = "수정 후 이름";
        ProgrammerResponse 개발자 = RestAssured
                .given().log().all()
                .contentType(ContentType.JSON)
                .body(new ProgrammerCreateRequest(
                        "userId",
                        "abc123!",
                        수정전이름,
                        24,
                        "email",
                        1,
                        "백엔드",
                        "안녕하세요",
                        "없음"))
                .when()
                .post("/programmers")
                .then().log().all()
                .statusCode(200)
                .extract()
                .as(ProgrammerResponse.class);
                
        //로그인
        String token = jwtProvider.createToken(개발자.id());
        
        ProgrammerResponse 개발자수정 = RestAssured
                .given().log().all()
                .contentType(ContentType.JSON)
                // TODO: "token" 실제 코드 작성
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                .body(new ProgrammerRequest(
                        "userId",
                        수정후이름,
                        24,
                        "chu@gmail",
                        1,
                        "벡앤드",
                        "안뇽",
                        "없음"))
                .when()
                .put("/programmers/my")
                .then().log().all()
                .statusCode(200)
                .extract()
                .as(ProgrammerResponse.class);
        assertThat(개발자.name()).isEqualTo(수정전이름);
        assertThat(개발자수정.name()).isEqualTo(수정후이름);
        assertThat(!개발자.name().equals(개발자수정.name())).isTrue();
    }
  • 비밀번호 수정
@Test
    void 비밀번호_수정() {
        String 수정전비번 = "수정 전 비번";
        String 수정후비번 = "수정 후 비번";
        ProgrammerResponse 개발자 = RestAssured
                .given().log().all()
                .contentType(ContentType.JSON)
                .body(new ProgrammerCreateRequest(
                        "userId",
                        수정전비번,
                        "추민영",
                        24,
                        "email",
                        1,
                        "백엔드",
                        "안녕하세요",
                        "없음"))
                .when()
                .post("/programmers")
                .then().log().all()
                .statusCode(200)
                .extract()
                .as(ProgrammerResponse.class);
                
        //로그인
        String token = jwtProvider.createToken(개발자.id());
        
        RestAssured
                .given().log().all()
                .contentType(ContentType.JSON)
                // TODO: "token" 실제 코드 작성
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                .body(new ProgrammerPasswordRequest(
                        수정후비번
                ))
                .when()
                .patch("/programmers/my")
                .then().log().all()
                .statusCode(200);
    }
  • 개발자 삭제
@Test
    void 개발자_삭제() {
        ProgrammerResponse 개발자 = RestAssured
                .given().log().all()
                .contentType(ContentType.JSON)
                .body(new ProgrammerCreateRequest(
                        "userId",
                        "abc123!",
                        "추민영",
                        24,
                        "email",
                        1,
                        "백엔드",
                        "안녕하세요",
                        "없음"))
                .when()
                .post("/programmers")
                .then().log().all()
                .statusCode(200)
                .extract()
                .as(ProgrammerResponse.class);
                
        //로그인
        String token = jwtProvider.createToken(개발자.id());
        
        RestAssured
                .given().log().all()
                .contentType(ContentType.JSON)
                // TODO: "token" 실제 코드 작성
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                .when()
                .delete("/programmers/my")
                .then().log().all()
                .statusCode(200);
    }

내 테스트는 끝났는데, 다른 사람의 테스트가 문제가 생겼다. 내가 분리한 테스트 격리 부분에서 Alter를 뺀 부분을 사용해야되는 문제가 생겼다.

🔥 1. 첫번째 문제
like라는 엔티티가 있다. 근데 table에는 like라는 테이블이 이미 지정되어 있기 때문에 like라는 테이블은 생성되지가 않는다. 따라서 like의 테이블 이름을 likes 로 들어가게 된다.
❓근데 likes로 하면 내부에 List가 있는걸로 오해하지 않는가?
따라서 이걸 구분해주는 테스트격리 코드를 작성해야한다.

  1. 일단 like의 엔티티의 테이블 이름을 아래처럼 수정해준다.
@Table(name = "likes")

이렇게 해준다고 테이블에 들어가는 이름이 likes라고 지정되진 않는다. 따라서
2. databaseCleanup코드를 수정해준다.

 @Transactional
    public void execute() {
        entityManager.flush();
        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();
        for (String tableName : tableNames) {
            tableName = tableName.equals("like") ? tableName + "s" : tableName;
            entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();

            if (tableName.equals("company") || tableName.equals("programmer")) {
                continue;
            }
            entityManager.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN ID RESTART WITH 1").executeUpdate();
        }
        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
    }

테이블 이름이 like라면 likes를 붙여서 likes의 효과를 내주어 테이블 이름을 저장한다.

  1. 조건문을 사용해서 company와 programmer로 테이블 이름이 들어왔을 때와 아닐 때를 구별해준다.
    company와 programmer로 들어왔을 때는 set을 써주고,그게 아니면 alter를 써라

이렇게 수정하니까 alter도 사용하면서 테이블이름도 사용할 수 있게 된다.


추가로 set함수들을 따로따로 할 필요가 없다고 하셔서 update()함수로 묶어서 보기 편하게 하면 좋을 것 같다는 피드백이 있었다. 그래서 기존에 set함수였던 것들을 update()함수에 넣었다.

public void update(String userId,
                       String name,
                       String email,
                       int age,
                       int personalHistory,
                       Field fieldName,
                       String selfIntroduction,
                       String certificate){
    }

이렇게 엔티티안에 넣어두면 기존 service에서 사용했던 set함수를

programmer.setUserId(programmerRequest.userId());
        programmer.setName(programmerRequest.name());
        programmer.setAge(programmerRequest.age());
        programmer.setEmail(programmerRequest.email());
        programmer.setPersonalHistory(programmerRequest.personalHistory());
        programmer.setFieldName(programmerRequest.fieldName());
        programmer.setSelfIntroduction(programmerRequest.selfIntroduction());
        programmer.setCertificate(programmerRequest.certificate());

update()로 바꿔주면 된다.

programmer.update(programmerRequest.userId(),
                programmerRequest.name(),
                programmerRequest.email(),
                programmerRequest.age(),
                programmerRequest.personalHistory(),
                programmerRequest.fieldName(),
                programmerRequest.selfIntroduction(),
                programmerRequest.certificate());

😐 느낀점

1. 오류 메세지를 잘 보자 - 오류 메세지가 이상하면 실행을 해보면 나온다.
2. 다른 개발자가 아직 다 못한 부분이 있어서 내가 영향을 받는 경우는
최대한 대체해서 처음에 만들고, 나중에 수정해야한다.
3. 테스트 격리!! 중요함!!

0개의 댓글