기능
: 해당 개발 프로젝트가 수행해야 되는 기능에 대해 상세히 설명함.필수 입력 데이터
: 해당 기능을 사용하기 위해서 반드시 입력되어야 하는 데이터를 말함.CASE
: 각 기능들의 유효성 처리, 특정 조건, 예외 상황 발생 시 조치 등을 적었다. 한걸음 더 부분에 해당구현방안
: 해당 기능을 어떻게 구현했는지를 적음자바 버전이 17인 것을 확인했다.
jpa, h2, mysql, spring web 종속성을 설치했다.
그 외에더 lombok 어노테이션도 프로젝트에 포함시켰다.
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
@RestController
@Controller
public class EmployeeContoller {
@GetMapping("/hello")
public String testString(@RequestParam String name){
return "Hello, " + name;
}
}
웹 화면에 Hello 가 출력 되는 것을 확인했다. 잘 설치 되었으니 다음으로 넘어가자
해당 테이블의 ERD는 다음과 같다.
직원과 팀은 N:1 관계이다. 팀 하나에 팀이 여러명이 있을 수 있다.
employee테이블과 team테이블을 생성하는 ddl문을 작성했다.
해당 테이블들의 크게 두 가지가 있다.
not null: work_start_date
를 제외한 모든 프로퍼티를 not null
로 처리해서 db에 들어갈 데이터가 누락되지 않게 했다. 이를 통해 DB 자체에서도 유효성 검사를 할 수 있도록 했다.
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)
);
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;
}
처음에 동일한 이름이 등록되는 것을 막기 위해 경고 메시지를 출력하려 했지만, 다른 스터디원의 피드백을 듣고 지나치게 과한 처리라고 생각하고 삭제 했다.
- 한국에서 동명이인만 해도 수십명이 되는 경우가 있다.
role
)은 MANAGER
와 MANAGER
외의 다른 문자열 입력을 허용하지 않는다. 매니저와 직원은 직무 외의 다른 직무 등록은 허용하지 않는다.이 부분은 Role Enum 클래스에
MANAGER
와MANAGER
만 등록 해둬서 다른 문자열 입력은 받지 않도록 했다.
컨트롤러
@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> {
}
팀 엔티티와 팀 컨트롤러, 팀 서비스, 팀 리포지토리 클래스를 각각 만들어준다.
팀 등록 기능 구현시 팀 이름만 입력 받으면 되서 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> {
}
팀 서비스 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;
}
@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) 가 명시 되어야 한다.
@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());
}
@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이나 주민번호로 아이디 중복을 막았었던 것 같다.
그 외 피드백
@Transaction(readOnly=True)
로 바꾸는 것이 좋다.readOnly=True로 지정하여 읽기 전용 메서드로 지정되면 jpa에서 변경감지, 스냅샷 저장을 하지 않는다.
메모리상 성능 이점을 크게 누릴 수 있다.
동일한 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
아래 메시지는 스프링 빈에서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 어노테이션을 인스턴스로 생성하는 클래스이다.
SpringBoot
공식 문서📋 데이터 모델링 개념 & ERD 다이어그램 작성 💯 총정리
[[2024_03_04 기능 명세서 작성 하기]]
SpringBoot
공식 문서