공부/인프런 워밍업 클럽_BE

[인프런 워밍업 클럽_0기] BE 미니 프로젝트 1단계

데부한 2024. 3. 3. 04:11
반응형

이미지를 클릭하면 해당 페이지로 이동합니다.

강의 출처

 

자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인

Java와 Spring Boot, JPA, MySQL, AWS를 이용해 서버를 개발하고 배포합니다. 웹 애플리케이션을 개발하며 서버 개발에 필요한 배경지식과 이론, 다양한 기술들을 모두 학습할 뿐 아니라, 다양한 옵션들

www.inflearn.com

 

 

기술 스택

  • 자바 17 버전
  • Spring Boot 3.2.3 버전
  • JPA
  • MySQL

 

프로젝트 1단계

 

반응형

 

프로젝트 생성

 

DB 설계

  • Team 테이블
    • name - String - 팀 이름 - 필수
    • manager - String - 팀 매니저 이름. null값 허용
    • memberCount - int - 팀 인원 수
  • Employee 테이블
    • name - String - 직원 이름
    • teamName - String - 소속 팀 이름
    • role - ENUM - 직급(매니저, 멤버)
    • birthday - LocalDate - 생일
    • workStartDate - LocalDate - 입사일

 

API 스펙 정하기

팀 등록하기

  • 팀을 등록한다.
  • HTTP Method > POST
  • HTTP Path > /team
  • HTTP Param > 쿼리 파라미터 String name
  • 반환 타입 > 200 OK

 

직원 등록하기

  • 직원을 등록한다.
  • HTTP Mehtod > POST
  • HTTP Path > /employee
  • HTTP Param > body, String name, String role, String temaName, LocalDate workStartDate, LocalDate birthday
  • 반환 타입 > 200 OK

 

팀 조회하기

  • 모든 팀의 정보를 한 번에 조회함
  • HTTP Method > GET
  • HTTP Path > /teams
  • HTTP Param > X
  • 반환 타입 > JSON

 

직원 등록하기

  • 모든 직원을 한 번에 조회함
  • HTTP Mehtod > GET
  • HTTP Path > /empolyees
  • HTTP Param > X
  • 반환 타입 > JSON

 

반응형

 

프로젝트 설정

MySQL 설정 + JPA 설정

  • build.gradle
// MySQL
runtimeOnly 'com.mysql:mysql-connector-j'

 

  • Database 설정
create database works; // database 생성

use works; // 만든 database 사용

 

  • 인텔리제이 MySQL 연결

 

  • application.yml
spring:
  datasource:
    url: "jdbc:mysql://localhost/works"
    username: "root"
    password: 
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
        show_sql: true
        format_sql: true

 

Team 등록하기 구현

Domain

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name; // 팀 이름
    private String manager; // 팀 매니저 이름
    private int memberCount; // 팀 인원 수

}

 

Controller

@PostMapping("/team")
public void addTeam(@RequestParam String name) {
    teamService.addTeam(name);
}

 

Service

@Transactional
public void addTeam(String name) {
    teamRepository.save(new Team(name));
}

 

Repository

public interface TeamRepository extends JpaRepository<Team, Long> {
}

 

반응형

 

  • 포스트맨 테스트

 

예외 사항 추가

  • team 이름이 중복인 경우

TeamService

@Transactional
public void addTeam(String name) {

    boolean isDuplicate = teamRepository.existsByName(name);
    if(isDuplicate) throw new IllegalArgumentException("팀 이름이 중복되었습니다.");
    teamRepository.save(Team.builder().name(name).build());
}

 

TeamRepository

boolean existsByName(String name);

 

  • 팀 이름이 null이거나 중복인 경우
if(name == null || name.isBlank()) throw new IllegalArgumentException("팀 이름은 빈 값을 저장할 수 없습니다.");

 

테스트 - 중복일 경우

 

테스트 - 값이 비어있을 경우

 

반응형

 


 

Employee 등록하기 구현

Domain

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name; // 직원 이름

    @ManyToOne
    @JoinColumn(nullable = false, name = "team_name")
    private Team team;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private Role role; // 역할

    @Column(nullable = false)
    private LocalDate workStartDate; // 입사일

    @Column(nullable = false)
    private LocalDate birthday; // 생일
}

개발을 해보니까 team의 id보다 name을 키로 갖고 매핑을 하는 게 더 나을 것 같다. id로 매핑하면 직원을 조회할때마다 team의 id로 name을 찾는 추가적인 로직이 들어가야하기 때문..! String으로는 한번도 id를 줘본적이 없어서 맞는 방식인가 싶기도 한데 구글링해보니까 String으로도 주는 것 같아서 일단 Team의 도메인을 수정했다.

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {

//    @Id
//    @GeneratedValue(strategy = GenerationType.IDENTITY)
//    private Long id;

    @Id
    @Column(nullable = false)
    private String name; // 팀 이름
    private String manager; // 팀 매니저 이름
    private int memberCount; // 팀 인원 수

    public void setManager(String name) {
        this.manager = name;
    }

    public void addMember() {
        this.memberCount++;
    }
}

 

TeamRepository도 수정

public interface TeamRepository extends JpaRepository<Team, String> {
    
}

 

TeamService 수정

/**
 * 새로운 팀을 등록한다.
 * @param name
 */
@Transactional
public void addTeam(String name) {

    if(name == null || name.isBlank()) throw new IllegalArgumentException("팀 이름은 빈 값을 저장할 수 없습니다.");

    boolean isDuplicate = teamRepository.existsById(name);
    if(isDuplicate) throw new IllegalArgumentException("팀 이름이 중복되었습니다.");
    teamRepository.save(Team.builder().name(name).build());
}

 

반응형

 

Controller

@PostMapping
public void addEmployee(@RequestBody EmployeeAddRequest request) {
    employeeService.addEmployee(request);
}

 

Dto

@Getter
@AllArgsConstructor
public class EmployeeAddRequest {

    private String name;
    private Role role;
    private String teamName;
    private LocalDate workStartDate;
    private LocalDate birthday;

}

 

Service

/**
 * 직원을 등록한다.
 * @param request
 */
@Transactional
public void addEmployee(EmployeeAddRequest request) {
    // team 객체 가져오기
    Team team = teamRepository.findById(request.getTeamName())
            .orElseThrow(IllegalArgumentException::new);

    employeeRepository.save(Employee.builder()
                            .name(request.getName())
                            .team(team)
            .role(request.getRole())
            .workStartDate(request.getWorkStartDate())
            .birthday(request.getBirthday()).build());

    // 만약 역할이 매니저면 team에 manager 등록
    if(request.getRole() == Role.MANAGER) {
        team.setManager(request.getName());
    }

    //팀 인원 수 + 1
    team.addMember();
}

 

  • 포스트맨 테스트

empolyee 테이블
team 테이블

테스트를 하다보니 문제점이 생긴 걸 볼 수 있다. 한 팀에 여러 명의 매니저가 생기는 문제를 해결해줘야할 거 같다.

 

반응형

 

예외 사항 추가

팀에 팀장은 바뀔 수 있는 존재니까 기존 팀에 MANAGER가 있다고 새로운 팀장을 저장못하면 말이 안되니 지금 저장 형태는 맞다고 생각된다. 다만 새로운 MANAGER가 등록이 됐을 때 기존 MANAGER였던 직원의 역할을 MEMBER로 변경해주자.

Team domain 클래스에 역할을 바꿔주는 changeRole() 메서드를 추가했다. Team이 담긴 employee List가 필요해서 필드를 추가하고 @OneToMany로 매핑해주었다.

@OneToMany(mappedBy = "team")
private List<Employee> employees = new ArrayList<>();

 

그리고 현재 기준 역할이 변경되는 순간은 EmployeeService에서 Team의 setManager() 밖에 없으니 Team의 setManager()에서 changeRole()를 호출하게 했다.

public void setManager(String name) {
    changeRole();
    this.manager = name;
}

 

private void changeRole() {
    if(this.manager == null) return;

    Employee findEmployee = this.employees.stream()
            .filter(employee -> employee.getRole() == Role.MANAGER)
            .findFirst()
            .orElseThrow(IllegalArgumentException::new);

    findEmployee.changeRole(Role.MEMBER);
}

 

Employee domain에서도 changeRole()을 추가했다.

public void changeRole(Role role) {
    this.role = role;
}

 

  • 포스트맨 테스트

employee 기존 데이터 삭제 후 다시 처음부터 넣어줬다.

 

잘 변경된 것을 확인할 수 있다.

 

반응형

 


Team 조회하기 구현

Controller

@GetMapping("/teams")
public List<TeamListResponse> getTeamList() {
    return teamService.getTeamList();
}

 

Dto

@Getter
@AllArgsConstructor
public class TeamListResponse {

    private String name;
    private String manager;
    private int memberCount;

    public TeamListResponse(Team team) {
        this.name = team.getName();
        this.manager = team.getManager();
        this.memberCount = team.getMemberCount();
    }
}

 

Service

/**
 * 모든 팀을 조회한다.
 * @return List
 */
@Transactional(readOnly = true)
public List<TeamListResponse> getTeamList() {
    return teamRepository.findAll().stream()
            .map(TeamListResponse::new)
            .collect(Collectors.toList());
}

 

  • 포스트맨 테스트

조회하기 전에 team 데이터를 두어개 더 넣어놓자.

 

team 조회를 해보면!

잘 된다.

 

반응형

 


Employee 조회하기 구현

Controller

@GetMapping("/employees")
public List<EmployeeListResponse> getEmployeeList() {
    return employeeService.getEmployeeList();
}

 

Dto

@Getter
@AllArgsConstructor
public class EmployeeListResponse {
    private String name;
    private String teamName;
    private Role role;
    private LocalDate birthday;
    private LocalDate workStartDate;

    public EmployeeListResponse(Employee employee) {
        this.name = employee.getName();
        this.teamName = employee.getTeam().getName();
        this.role = employee.getRole();
        this.birthday = employee.getBirthday();
        this.workStartDate = employee.getWorkStartDate();
    }
}

 

Service

/**
 * 모든 직원을 조회한다.
 * @return List
 */
public List<EmployeeListResponse> getEmployeeList() {
    return employeeRepository.findAll().stream()
            .map(EmployeeListResponse::new)
            .collect(Collectors.toList());
}

 

  • 포스트맨 테스트

조회하기 전에 데이터를 두어개 정도 추가적으로 넣어주자.

데이터가 빵빵해졌다

 

수치스러운 404 에러~!

컨트롤러를 확인해보니 클래스 레벨에 @RequestMapping("/employee")를 냅다 써버려서 요청 url를 받아들이지 못한거였다. 수정하고 다시 조회!

성공

 

반응형

 

추가적으로 발견한 에러 사항

직원을 등록할 때 JSON의 role이 대문자여야지만 맵핑이 가능하다.

manager, member가 대문자가 아니면 바인딩 관련 에러가 발생한다.

스프링에서 제공하는 StringToEnumConverter가 아래와 같이 동작하기 때문이다.

값에 대한 별다른 컨트롤이 없다! 그래서 대소문자를 구분하지 않게 만드려면 컨버터를 추가하는 방법, 아니면 아래와 같이 application.yml에 설정을 추가해주면 된다.

spring:
  jackson:
    mapper:
      accept-case-insensitive-enums: true

 

다시 테스트!

성공!

 

다음 포스트

 

[인프런 워밍업 클럽_0기] BE 미니 프로젝트 2단계 (개발중)

강의 출처 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인 Java와 Spring Boot, JPA, MySQL, AWS를 이용해 서버를 개발하고 배포합니다. 웹 애플리케이션

devhan.tistory.com

 


Reference

- https://blog.naver.com/PostView.naver?blogId=simpolor&logNo=221598980908

반응형