14일차 미니프로젝트 1단계_인프런워밍업BE0

nakyeonko3·2024년 3월 7일
0
post-thumbnail

소스코드 링크

프로젝트 1단계 요구사항 구현

프로젝트 1단계 요구사항

  • 직원 등록 기능
  • 팀 등록 기능
  • 팀 조회 기능
  • 직원 조회 기능
  • 등록된 직원의 팀 등록 기능

프로젝트 1단계 구현


개발 스펙


해당 문서 설명


  • 미니프로젝트 프로젝트 1단계 2단계를 상세히 명시해야 함.
  • 기능: 해당 개발 프로젝트가 수행해야 되는 기능에 대해 상세히 설명함.
  • 필수 입력 데이터: 해당 기능을 사용하기 위해서 반드시 입력되어야 하는 데이터를 말함.
  • CASE: 각 기능들의 유효성 처리, 특정 조건, 예외 상황 발생 시 조치 등을 적었다. 한걸음 더 부분에 해당
  • 구현방안: 해당 기능을 어떻게 구현했는지를 적음

프로젝트 준비


0. 자바 버전 확인, 17 버전 확인

자바 버전이 17인 것을 확인했다.

1. spring start web

jpa, h2, mysql, spring web 종속성을 설치했다.
그 외에더 lombok 어노테이션도 프로젝트에 포함시켰다.

2. application. yml 파일 구성

ddl-auto: create 옵션을 이용해서 스프링부트 서버를 시작 할 때 기존 테이블을 삭제 하도록 바꿨다.
테스트를 편하게 하기 위해서 바꿨다.


spring:
  config:
    activate:
      on-profile: local
  datasource:
    url: "jdbc:h2:mem:company_timeclock;MODE=MYSQL;"
    username: "sa"
    password: ""
    driver-class-name: org.h2.Driver
  jpa:
    properties:
      hibernate:
        ddl-auto: create
        show_sql: true
        format_sql: true
server:
  port: 58080
---
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: "jdbc:mysql://localhost/company_timeclock"
    username: "root"
    password: ""
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect
server:
  port: 58080

3. controller만 만들어서 스프링 프로젝트 설정 확인하기

@RestController
@Controller
public class EmployeeContoller {
    @GetMapping("/hello")
    public String testString(@RequestParam String name){
        return "Hello, " + name;
    }
}

웹 화면에 Hello 가 출력 되는 것을 확인했다. 잘 설치 되었으니 다음으로 넘어가자

3. sql 쿼리문 작성

해당 테이블의 ERD는 다음과 같다.

직원과 팀은 N:1 관계이다. 팀 하나에 팀이 여러명이 있을 수 있다.

employee테이블과 team테이블을 생성하는 ddl문을 작성했다.

해당 테이블들의 크게 두 가지가 있다.

  1. not null: work_start_date를 제외한 모든 프로퍼티를 not null로 처리해서 db에 들어갈 데이터가 누락되지 않게 했다. 이를 통해 DB 자체에서도 유효성 검사를 할 수 있도록 했다.

  2. unique key: 팀이름을 unique key로 지정해서 팀 이름이 중복 되서 들어가지 않게 했다

create table employee
(
    id              bigint auto_increment,
    name            varchar(255) not null,
    role            tinyint not null,
    birthday        date         not null,
    team_name       varchar(255),
    work_start_date date,
    primary key (id)
);


create table team
(
    id   bigint auto_increment,
    name varchar(255) not null,
    primary key (id),
    unique key (name)
);

4. Employee, Team 엔티티

Employee 엔티티와 Team 엔티티를 코드를 만들었다.
특징이 될 만한 것은 Role 직무 변수를 아래처럼 Role Enum 클래스를 이용해서 작성하였다.

public enum Role {
    MANAGER, MEMBER
}

직원 엔티티

@Entity
@Getter
public class Employee {

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

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Role role;
    @Column(nullable = false)
    private LocalDate birthday;

    @Column(nullable = false)
    private LocalDate workStartDate;

    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;

}

팀 엔티티

@Entity
@Getter
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(unique = true, nullable = false)
    private String name;

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

    protected Team() {
    }

Employee 클래스의 getTeam 메서드는 나중에 추가했다.
직원 전체를 조회 할 때 가끔 직원 중에 팀이 지정되지 않은 경우에 NPE 문제가 발생해서 getTeam 메서드를 추가했다. 밑에서 상세히 설명하겠다.

직원 클래스의 getTeam 메서드

    public Team getTeam() {
        if(this.team ==null) {
            return new Team();
        }
        return team;
    }

직원 등록 기능


기능

  • 직원을 등록 할 수 있어야 한다.
  • 같은 이름을 가진 직원 등록을 허용한다.
  • 필수 입력 데이터: 직원 이름(name), 직무(role), 회사에 들어온 날짜(workStartDate), 생일(birthday), 직원 아이디(id)

CASE

  • 해당 기능 수행 시 필수 정보가 전부 다 등록 되지 않으면 다시 등록 절차를 수행한다.

처음에 동일한 이름이 등록되는 것을 막기 위해 경고 메시지를 출력하려 했지만, 다른 스터디원의 피드백을 듣고 지나치게 과한 처리라고 생각하고 삭제 했다.

- 한국에서 동명이인만 해도 수십명이 되는 경우가 있다. 
  • 직무(role)은 MANAGERMANAGER 외의 다른 문자열 입력을 허용하지 않는다. 매니저와 직원은 직무 외의 다른 직무 등록은 허용하지 않는다.

    이 부분은 Role Enum 클래스에 MANAGERMANAGER 만 등록 해둬서 다른 문자열 입력은 받지 않도록 했다.

상세 구현

컨트롤러

@RestController
@RequestMapping("/api/v1/employee")
class EmployeeController {

    private final EmployeeService employeeService;

    public EmployeeController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @PostMapping
    public void saveEmployee(@RequestBody EmployeeSaveRequest employee) {
        employeeService.saveEmployee(employee);
    }
    }

서비스

@Service
public class EmployeeService {

    private final EmployeeRepository employeeRepository;
    private final TeamRepository teamRepository;

    public EmployeeService(EmployeeRepository employeeRepository, TeamRepository teamRepository) {
        this.employeeRepository = employeeRepository;
        this.teamRepository = teamRepository;
    }

    @Transactional
    public void saveEmployee(EmployeeSaveRequest request) {
        employeeRepository.save(new Employee(request));
    }
   }

리포지토리

public interface EmployeeRepository extends JpaRepository<Employee,Long> {  
}

팀 등록 기능


기능

  • 팀을 등록 할 수 있는 기능
  • 필수 입력 데이터: 팀 이름(name)

CASE

  • 해당 기능 수행 시 필수 정보가 전부 다 등록 되지 않으면 다시 등록 절차를 수행한다.
  • 한 번에 1개의 팀을 등록 가능, 한 번에 2개의 팀을 등록 할 수 없다.
  • 팀 이름은 중복 될 수 없다. 같은 이름의 팀 등록시 오류 메시지를 출력한다.

상세 구현

팀 엔티티와 팀 컨트롤러, 팀 서비스, 팀 리포지토리 클래스를 각각 만들어준다.

팀 등록 기능 구현시 팀 이름만 입력 받으면 되서 DTO는 만들지 않았다.

컨트롤러에서 팀 이름을 받고
서비스에서 팀 이름을 도메인을 가지고 리포지토리에 전달하도록 했다.

  • 컨트롤러
@RestController
@RequestMapping("/api/v1/team")
public class TeamController {
    final private TeamService teamService;

    public TeamController(TeamService teamService) {
        this.teamService = teamService;
    }

    @PostMapping
    public void saveTeam(@RequestParam String name) {
        teamService.saveTeam(name);
    }
}
  • 서비스
@Service
public class TeamService {
    final private TeamRepository teamRepository;

    public TeamService(TeamRepository teamRepository) {
        this.teamRepository = teamRepository;
    }

    @Transactional
    public void saveTeam(String name) {
        teamRepository.save(new Team(name));
    }
    }
  • 리포지토리
public interface EmployeeRepository extends JpaRepository<Employee,Long> {  
}

직원의 팀 등록 기능


기능

  • 직원이 처음에 팀을 등록하지 않기 때문에 등록된 기능이 팀을 등록할 수 있도록 이 기능을 만들었다.
  • 등록된 직원이 팀을 등록할 수 있는 기능이다.
  • 필수 입력 데이터: 팀 이름(name), 직원(id)

CASE

  • 해당 기능 수행 시 필수 정보가 전부 다 등록 되지 않으면 다시 등록 절차를 수행한다.
  • 한 번에 1개의 직원을 등록 가능, 한 번에 2명의 직원을 등록 할 수 없다.
  • 입력된 직원 아이디를 검색하고 검색 결과가 없다면 오류 메시지를 출력한다. 등록되지 않은 직원의 등록을 허용하지 않는다.
  • 팀 이름을 검색하고 검색 결과가 없다면 오류 메시지를 출력한다. 등록되지 않은 팀의 등록을 허용하지 않는다.

상세 구현

  • 팀 서비스 TeamService.java의 updateEmployeeTeam메서드 의 설명
    팀 이름직원 아이디를 데이터를 받는다.
    해당 팀 이름을 검색하는 쿼리문을 날리고, 해당 직원 이름을 검색하는 쿼리를 각각 날린 다음에
    Employee클래스의 updateTeamName메서드를 이용해서 검색 결과로 나온 팀과 직원을 연결 시켰다.

  • 팀 컨트롤러

    @PutMapping()
    public void updateEmployeeTeamName(@RequestBody EmployeeUpdateTeamRequest request) {
        employeeService.updateEmployeeTeam(request.getTeamName(), request.getEmployeeId());
    }
  • 팀 서비스
    @Transactional
    public void updateEmployeeTeam(String teamName, Long employeeId) {
        Employee employee = employeeRepository.findById(employeeId)
                .orElseThrow(IllegalArgumentException::new);

        Team team = teamRepository.findByName(teamName)
                .orElseThrow(IllegalArgumentException::new);

        employee.updateTeamName(team);
    }
  • 직원 클래스
    public void updateTeamName(Team team) {
        this.team = team;
    }

팀 조회 기능


기능

  • 모든 팀 조회 기능
  • 필수 입력 데이터: 없음
  • 조회 데이터에는 팀이름(name), 매니저 이름(manager), 팀 인원수(memberCount)가 명시되어야함

CASE

  • 모든 팀의 정보를 한 번에 조회 할 수 있어야 한다.
  • 특정 입력 데이터 없이도 팀 조회 기능 사용 가능하다.
  • 팀 매니저 명은 누락될 수 있다. 특정 팀은 팀명만 등록되고 매니저 명은 등록되지 않을 수 있다.
  • 팀 인원수 변수는 만들었다가 제거했다. 이유는 sql 쿼리문 count를 이용해서 충분히 팀 인원수를 구할 수 있다.
  • 한 팀 당 다수의 매니저가 존재 할 수도 있다. 매니저의 숫자는 명시 되지 않았다.

상세 구현

  • 컨트롤러
    @GetMapping
    public List<TeamGetAllRespone> getTeams() {
        return teamService.getTeams();
    }
  • 서비스
    @Transactional(readOnly = true)
    public List<TeamGetAllRespone> getTeams() {
        List<Team> teams = teamRepository.findAll();
        return teams.stream()
                .map(TeamGetAllRespone::new)
                .collect(Collectors.toList());
    }

getTeams는 읽기 전용 메서드이므로 변경 감지 기능을 사용하지 않도록 readOnly = true로 바꾸었다.

팀 서비스 클래스의 getTeams 메서드는 teamRepository.findAll을 이용해서 팀 테이블에 저장된 모든 팀의 (팀이름, 팀아이디)를 가져온다.

// TeamService.java 
List<Team> teams = teamRepository.findAll();
    return teams.stream()
                .map(TeamGetAllRespone::new)
                .collect(Collectors.toList());

TeamGetAllRespone 클래스의 생성자는 팀이름, 팀매니저명, 팀 인원수를 이용해서 객체를 생성한다.

//  TeamGetAllRespone.java
public TeamGetAllRespone(Team team) {
        this(team.getName(), team.getManagerName(), team.getEmployees().size());
    }

팀 엔티티 클래스의 getManagerName 메서드는 자신과 연결된 직원들 중에서 매니저인 직원들을 찾아서 리턴한다.


// Team.java
public List<String> getManagerName() {
        return this.getEmployees()
                .stream()
                .filter(employee -> employee.getRole()
                        .equals(Role.MANAGER))
                .map(Employee::getName)
                .collect(Collectors.toList());
    }

직원 조회 기능


기능

  • 모든 직원의 정보를 한 번에 조회할 수 있어야 함.

  • 조건 없이 직원 정보를 전체를 조회 할 수 있음.

  • 필수 입력 데이터: 없음

  • 조회 된 데이터에는 직원 이름( name), 매니저명(manager), 직무(role), 생일(birthday), 회사에 들어온 날짜(workStartDate) 가 명시 되어야 한다.

CASE

  • 모든 직원의 정보를 한 번에 조회 할 수 있어야 한다.
  • 특정 입력 데이터 없이도 직원 조회 기능 사용 가능하다.
  • 직원 아이디를 이용해 조회해야 한다. 직원 이름은 동일한 이름이 여러 개 존재할 수 있다.

상세 설계

  • 컨트롤러
    @GetMapping
    public List<TeamGetAllRespone> getTeams() {
        return teamService.getTeams();
    }
  • 서비스
    @Transactional(readOnly = true)
    public List<TeamGetAllRespone> getTeams() {
        List<Team> teams = teamRepository.findAll();
        return teams.stream()
                .map(TeamGetAllRespone::new)
                .collect(Collectors.toList());
    }
  • TeamGetAllRespone.java DTO
@Getter
public class EmployeeGetAllResponse {
    final private String name;
    final private String teamName;
    final private Role role;
    final private LocalDate birthday;
    final private LocalDate workStartDate;

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


팀원 피드백

  • count 함수를 이용해서 팀 인원수를 구할 수 있으니, 팀 인원 수를 DB에 굳이 저장을 할 필요가 없다.

  • 생일(birthday) 부분은은 dateime을 쓰기 보다는 date를 쓰는 것을 추천함.

  • sql은 snake case로 작성 하는 걸 추천함  user_login_log

  • url은 케밥 케이스로 작성하는 걸 추천한다. user-login-log

  • 동명이인에 대한 검사는 이메일이나 email이나 주민번호 같은 것으로 검사하는 것이 좋다.
    - 미국은 최소 수십명의 동명이인이 존재한다. 동명이인이 입력 될 때 마다 동일한 이름이 입력될 때 마다 경고 메시지를 보내는 것은 과하다.
    - 생각해보면 email이나 주민번호로 아이디 중복을 막았었던 것 같다.

  • 의문점? 같은 팀이름이 중복되서 등록되지 않도록, Mysql에서 테이블에 팀이름 필드에 unique key제약 조건을 걸어주는 것이 괜찮을까? 아니면 Spring에서 service 레이어에서 팀이름 중복을 검사하는 것이 좋을까?

그 외 피드백

  • 서비스 클래스에서 읽기 전용 메서드는 트랜잭션을 @Transaction(readOnly=True)로 바꾸는 것이 좋다.

readOnly=True로 지정하여 읽기 전용 메서드로 지정되면 jpa에서 변경감지, 스냅샷 저장을 하지 않는다.
메모리상 성능 이점을 크게 누릴 수 있다.

에러와 싸움기록


Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource. ambiguous mapping. Cannot map method

동일한 url을 가진 레스트컨트롤러 메서드가 존재하면 이런 에러가 난다.

.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Ambiguous mapping. Cannot map 'employeeController' method 
com.group.companytimeclockapp.controller.EmployeeController#saveEmployee(EmployeeSaveRequest)
to {POST [/employee]}: There is already 'employeeController' bean method

Error: creating bean with name 'requestMappingHandlerMapping' defined in class path resource.

아래 메시지는 스프링 빈에서requestMappingHandlerMapping 를 생성하는 중에 에러가 발생했다는 뜻이다.

아래 에러 로그 영문을 해석 해보면 employeeController가 {POST [/employee]} 에 매핑된 메서드가 존재한다는 뜻이다.

같은 uri 주소를 가지는 메서드가 2개 이상 존재하기 때문에 이런 오류가 난 것이다.
`

.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Ambiguous mapping. Cannot map 'employeeController' method 
com.group.companytimeclockapp.controller.EmployeeController#saveEmployee(EmployeeSaveRequest)
to {POST [/employee]}: There is already 'employeeController' bean method

에러 메시지를 확인하고 바로 어디서 에러가 났는지 확인 할 수 있었기 때문에 큰 문제가 아니였다.

RequestMappingHandlerMapping 은 스프링 웹 MVC 프레임워크 코드의 일부로 @Controller 클래스 안에 정의된 @RequestMapping 어노테이션을 인스턴스로 생성하는 클래스이다.

참고


📋 데이터 모델링 개념 & ERD 다이어그램 작성 💯 총정리

[[2024_03_04 기능 명세서 작성 하기]]

참고


profile
블로그 이전 작업중. 올해(2024년)까지만 여기에 블로그글만 올릴 것임.

0개의 댓글