코즐린 프로젝트의 취지는 인터넷강의나, 책을 읽으면서 하는 공부가 맞지않고 무언가 혼자 만들어보면서 숙달되는게 좋아서 무작정 시작한 풀스택 프로젝트이다.
오늘은 나의 멘토님과 나눴던 이야기를 바탕으로 내가 얼마나 아무것도 모르고 눈에 보이는 것만 신경쓰고 만들었는지를 더욱 더 알게 되면서 기록을 남겨놓고 하나씩 해결 해 가보도록 하겠다.
1번째 문제 spring에서 나눠놓은 레이어드 아키텍처를 항상 잘 생각하자.
@PostMapping("/user")
public ResponseEntity<BasicResponse> insertUser(@RequestBody User user) {
BasicResponse basicResponse = new BasicResponse();
UserJob userJob = UserJob.builder().userJobId(user.getUserId()).userJobEnterdYn("N").userJobCareerYn("N").build();
user.setUserPassword(pwEncoder.encode(user.getUserPassword()));
User userIdCheck = service.selectUserById(user.getUserId());
if (userIdCheck.getUserId() != null) {
basicResponse = basicResponseForm(HttpStatus.CONFLICT, "이미 사용중인 ID 입니다.", null);
} else {
basicResponse = basicResponseForm(HttpStatus.OK, "회원가입이 완료 되었습니다.", null);
service.insertUser(user);
service.insertUserJob(userJob);
}
ResponseEntity<BasicResponse> response = new ResponseEntity<>(basicResponse, basicResponse.getHttpStatus());
return response;
}
위의 코드는 회원가입 엔트리포인트 이다. 문제는 .. spring 이 나눠놓은 레이어를 박살 냈다는 것이다.
show process list
명령어를 사용해서 DB의 프로세스가 현재 어떻게 진행되고 있는지를 꼭 확인하는 습관을 기르자.( 진행중 )Read-Write Lock
패턴에 대해서 이해하고 정리해보자. ( 해결 )Controller
라는 단어는 JSP나 Thymeleaf 같은 화면을 넘겨줄 때 사용한다고 한다. 나의 프로젝트로 Controller
라는 단어보다 그냥 편하게 api 라는 걸로 사용하면 좋을 듯 하다. ( 해결 )피드백 문제 해결
@Transactional 은 무슨 역할을 해주고 어떻게 사용해야 하는 것 일까 ? 또한 그동안은 어노테이션을 달아야만 업데이트 및 삭제가 가능한 줄 알았지만 없어도 가능한 것을 알았고 정확한 사용과 의미에 대해 알아보자.
JPA(Java Persistence API)의 @Transactional은 트랜잭션 관리를 위해 사용되는 애너테이션입니다. 이 애너테이션을 메서드나 클래스에 적용하면 해당 메서드 또는 클래스의 실행이 트랜잭션 내에서 수행됩니다.
트랜잭션은 여러 개의 데이터 조작 작업을 논리적인 단위로 묶어서 원자성, 일관성, 격리성, 지속성 (ACID)을 보장하는 개념입니다. 트랜잭션 내에서 실행되는 작업은 모두 성공적으로 완료되거나 모두 롤백되어야 합니다. @Transactional 애너테이션은 이러한 트랜잭션을 관리하기 위해 사용됩니다.
트랜잭션 범위의 제어: @Transactional은 메서드 단위 또는 클래스 단위에서 트랜잭션을 적용합니다. 이는 일부 메서드만 트랜잭션 내에서 실행되도록 제어하거나, 다른 트랜잭션 설정이 필요한 경우에는 세밀한 제어가 어렵다는 의미입니다. 때로는 트랜잭션 범위를 더 작게 제어해야 할 때가 있으므로 상황에 맞게 사용해야 합니다.
성능 영향: 트랜잭션은 데이터베이스 조작 작업을 원자적으로 처리하기 위해 사용됩니다. 그러나 트랜잭션은 오버헤드를 동반합니다. 트랜잭션을 시작하고 커밋 또는 롤백하는 과정은 시간이 걸리고, 락을 유지하는 등의 추가 작업이 필요합니다. 특히 큰 규모의 데이터베이스 작업이나 성능이 중요한 환경에서는 트랜잭션 범위를 최소화하여 성능 저하를 최소화해야 합니다.
다중 데이터소스 사용: 여러 개의 데이터소스를 사용하는 경우, @Transactional은 한 데이터소스에 대한 트랜잭션을 처리합니다. 다중 데이터소스 간에 트랜잭션을 동기화하는 것은 복잡할 수 있으며, 이러한 경우에는 명시적인 트랜잭션 관리가 필요할 수 있습니다.
DB read ,wirte lock 에 대해
Read Lock은 데이터를 읽을 때 사용되는 동시성 제어 메커니즘입니다.
Read Lock이 설정된 트랜잭션이 데이터를 읽고 있는 동안 다른 트랜잭션이 해당 데이터를 수정하는 것을 막습니다.
Read Lock은 여러 트랜잭션이 동시에 동일한 데이터를 읽을 수 있도록 허용합니다.
예시: 여러 사용자가 동시에 동일한 데이터를 조회하는 경우, Read Lock을 사용하여 데이터의 일관성을 유지하고 충돌을 방지합니다.
Write Lock은 데이터를 수정할 때 사용되는 동시성 제어 메커니즘입니다.
Write Lock이 설정된 트랜잭션이 데이터를 수정하는 동안 다른 트랜잭션이 해당 데이터를 읽거나 수정하는 것을 막습니다.
Write Lock은 동시에 여러 사용자가 동일한 데이터를 수정하는 것을 방지하여 데이터의 일관성을 유지합니다.
예시: 여러 사용자가 동시에 동일한 데이터를 수정하는 경우, Write Lock을 사용하여 충돌을 방지하고 데이터의 정합성을 보장합니다.
2번째 문제 구현체가 하나면 인터페이스를 굳이 만들지 않는게 좋음. (해결)
public interface MailServiceInter {
// 메일 내용 작성
MimeMessage creatMessage(String to) throws MessagingException, UnsupportedEncodingException;
MimeMessage creatFindMessage(String to) throws MessagingException, UnsupportedEncodingException;
// 랜덤 인증코드 생성
String createKey();
// 메일 발송
String sendSimpleMessage(String to, String flag) throws Exception;
}
이러한 메일 서비스 인터페이스를 달고 구현체는 하나만 사용하고 있었다.
구현체가 하나면 인터페이스를 굳이 만들지 않고 하나의 파일에서 만드는게 낫다.
그 말은 즉 인터페이스를 둔다는 의미 자체가 구현체가 최소 2개이상이 있다 라는 의미 이기도 하다.
파일명도 애매하기 때문에 직관적으로 MailService 라고만 해도 될 것 같다.
3번째 문제 - 의존성 주입의 방법에 대해서 차이점을 알아보자.
private UserService service;
private DefaualtMailServiceInterImpl registerMail;
public UserController(UserService service, DefaualtMailServiceInterImpl registerMail) {
this.service = service;
this.registerMail = registerMail;
}
현재는 생성자 방식으로 주입을 하고 있다. 하지만..
롬복 생성자 인젝션
setter autowired 인젝션
등등 무엇으로 또 할 수 있으며 각자의 차이가 무엇인지 분석하고 알고가자.
피드백 문제 해결
생성자 주입은 클래스의 생성자를 통해 의존성을 주입하는 방식입니다.
주입 받을 의존성을 final로 선언할 수 있어 불변성을 보장할 수 있습니다.
객체 생성 시점에 의존성이 모두 주입되므로, 객체의 일관성과 안정성을 보장할 수 있습니다.
테스트하기 쉽고 의존성이 명시적으로 드러나므로 코드의 가독성과 유지보수성을 높일 수 있습니다.
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
public UserService(UserRepository userRepository, MemberService memberService) {
this.userRepository = userRepository;
this.memberService = memberService;
}
}
필드 주입은 의존성을 클래스의 필드에 직접 주입하는 방식입니다.
주입 받을 의존성을 public 또는 private 필드로 선언합니다.
코드의 양을 줄일 수 있고, 편리한 구현이 가능합니다.
하지만 의존성이 외부에서 직접 접근 가능하므로 캡슐화 원칙에 어긋날 수 있고, 테스트하기 어렵고 유연성이 떨어질 수 있습니다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
}
세터 주입은 의존성을 객체의 세터 메서드를 통해 주입하는 방식입니다.
의존성 주입 순서가 유연하게 조정될 수 있어서 일부 의존성만 필요한 경우에 유용합니다.
선택적으로 의존성을 주입할 수 있으므로, 의존성이 필수적이지 않은 경우 유연하게 처리할 수 있습니다.
하지만 세터 메서드가 많아지면 코드의 가독성이 저하될 수 있으며, 세터 메서드를 통해 잘못된 의존성을 주입할 수 있는 위험성도 있습니다.
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
}
캡슐화 원칙 위반: 필드 주입은 주입 받을 의존성을 public 또는 private 필드로 선언합니다. 이는 객체의 캡슐화 원칙을 위반하는 것입니다. 의존성이 외부에서 직접 접근 가능하므로 객체의 내부 상태를 외부에서 변경할 수 있으며, 객체의 일관성과 불변성을 유지하기 어렵게 됩니다.
유연성의 감소: 필드 주입은 의존성을 주입받을 필드를 final로 선언할 수 없으므로, 필드의 변경이 자유롭게 이루어질 수 있습니다. 이는 의존성이 변경될 경우 해당 필드를 사용하는 모든 부분을 수정해야 한다는 의미입니다. 이로 인해 유지보수성이 저하되며, 의존성 변경에 따른 리팩토링 비용이 증가합니다.
테스트의 어려움: 필드 주입은 의존성을 객체의 필드에 직접 주입하므로, 테스트할 때 의존성을 모의(mock) 객체로 대체하기 어렵습니다. 의존성을 목적에 맞게 대체하여 테스트하려면 필드에 접근할 수 있는 setter 메서드나 다른 방법을 통해 값을 변경해야 합니다.
의존성 감추기 어려움: 필드 주입은 의존성이 클래스의 필드에 직접 노출되므로, 해당 클래스가 어떤 의존성을 사용하는지를 외부에 노출시킵니다. 이는 클래스의 구현 세부 사항을 외부에 노출시키고, 의존성의 변경으로부터 자유로워지기 어렵게 만듭니다.
4번째 문제 - 컨트롤러에서 바로 엔티티를 넘겨주지말자.
@GetMapping("/user")
public List<User> selectAllUser() {
return service.selectAllUser();
}
@GetMapping("/user/{id}")
public User selectUserById(@PathVariable String id) {
return service.selectUserById(id);
}
이 코드들은 현재 컨트롤러 에서 엔티티를 직접 내어주고 있다.. 생각을 못했는데 이건 정말로 최악의 행위였다. db의 변동사항에 따라서 프론트 쪽에서 맞춰놓은 규칙을 깨버릴 수 있으니까..
피드백 문제 해결
관심사의 분리 (Separation of Concerns):
컨트롤러는 주로 클라이언트 요청을 처리하고 응답을 반환하는 역할을 수행합니다. 데이터 액세스는 주로 서비스 레이어 또는 리포지토리 레이어에서 처리되어야 합니다. 이를 통해 코드를 더욱 모듈화하고 각 부분의 역할을 분리하여 관리하기 쉽게 만듭니다.
유연성과 확장성:
컨트롤러는 비즈니스 로직의 변경에 대한 영향을 최소화해야 합니다. 직접 데이터베이스를 반환하는 경우, 데이터베이스 구조의 변경이 컨트롤러까지 영향을 미칠 수 있습니다. 이는 유연성과 확장성을 제한하고, 코드를 수정해야 할 가능성을 높입니다.
보안:
데이터베이스에 직접 액세스하는 것은 보안 취약점을 가질 수 있습니다. 악의적인 사용자가 컨트롤러를 이용하여 민감한 데이터를 열람하거나 수정하는 등의 공격을 시도할 수 있습니다. 서비스 레이어를 통해 데이터 액세스를 추상화하고 필요한 보안 절차를 수행하는 것이 좋습니다.
따라서, 권장되는 방법은 컨트롤러에서는 서비스 레이어를 호출하여 데이터를 가져오고, 서비스 레이어에서는 데이터 액세스를 처리하도록 하는 것입니다. 이렇게 하면 코드의 유지보수성, 재사용성, 테스트 용이성 등을 향상시킬 수 있습니다.
@GetMapping("/user")
public List<UserDto> selectAllUser() {
List<User> users = service.selectAllUser();
return convertToDTO(users);
}
@GetMapping("/user/{id}")
public UserDto selectUserById(@PathVariable String id) {
User user = service.selectUserById(id);
return convertToDTO(user);
}
private List<UserDto> convertToDTO(List<User> users) {
List<UserDto> userDTOs = new ArrayList<>();
for (User user : users) {
UserDto userDTO = new UserDto();
userDTO.setUserId(user.getUserId());
userDTO.setUserPassword(user.getUserPassword());
userDTO.setUserBirth(user.getUserBirth());
userDTO.setUserAddr(user.getUserAddr());
userDTO.setUserJob(user.getUserJob());
userDTO.setUserPhone(user.getUserPhone());
userDTO.setUserName(user.getUserName());
userDTOs.add(userDTO);
}
return userDTOs;
}
private UserDto convertToDTO(User user) {
UserDto userDTO = new UserDto();
userDTO.setUserId(user.getUserId());
userDTO.setUserPassword(user.getUserPassword());
userDTO.setUserBirth(user.getUserBirth());
userDTO.setUserAddr(user.getUserAddr());
userDTO.setUserJob(user.getUserJob());
userDTO.setUserPhone(user.getUserPhone());
userDTO.setUserName(user.getUserName());
return userDTO;
}
dto 를 이용해 DB를 직접 리턴하지 않고 중간 가공 작업을 거쳐서 작업하였다.
5번째 문제 - JWT
현재 securiy + jwt 로 로그인 인증을 진행하고 있는데 강의와 구글링을 해가며 이해가 부족한상태에서 만든 코드기 때문에 만든 코드를 하나하나 해석해보자.
@PostMapping("/authenticate")
public ResponseEntity<JwtTokenResponse> generateToken(@RequestBody JwtTokenRequest jwtTokenRequest) {
var authenticationToken = new UsernamePasswordAuthenticationToken(jwtTokenRequest.getUsername(), jwtTokenRequest.getPassword());
var authentication = authenticationManager.authenticate(authenticationToken);
// 토큰 생성
var token = tokenService.generateToken(authentication);
User user = service.findByUserId(jwtTokenRequest.getUsername());
return ResponseEntity.ok(new JwtTokenResponse(token, user.getUserId(), user.getUserName(), user.getUserPhone(), user.getUserBirth(), user.getUserAddr(), user.getUserJob().getUserJobEnterdYn(), user.getUserJob().getUserDesiredJobGroupCareer(), user.getUserJob().getUserDesiredJobGroup(), user.getUserJob().getUserDesiredJob(), user.getUserJob().getUserJobSkill(), user.getUserJob().getUserLastCompany(), user.getUserJob().getUserLastJobGroup(), user.getUserJob().getUserLastJobGroupCareer(), user.getUserJob().getUserLastSchoolName(), user.getUserJob().getUserLastSchoolStatus(), user.getUserJob().getUserLastSchoolDept(), user.getUserJob().getUserJobCareerYn()));
}
JwtTokenRequest
객체의 username과 password를 사용하여 UsernamePasswordAuthenticationToken
객체를 생성합니다. 이 객체는 Spring Security의 인증 매니저를 통해 인증을 수행하는 데 사용될 것입니다.
authenticationManager
를 사용하여 authenticationToken
을 인증합니다. 이는 사용자의 인증 정보를 확인하고 유효한 경우 인증을 완료합니다. authentication 객체는 인증 결과를 담고 있다.
인증을 완료하고 필요한 정보와 토큰을 반환한다.
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=db_user
spring.datasource.password=db_password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
현재 코드에서는 엔트리포인트와 http status 코드에 따른 응답이 전부 중구난방이다. 내 프로젝트에 대한 API 문서를 작성해보고 200 이어도 항상 명확하게 같은 응답을 내뱉어줘야 한다.
또한 톰캣이 내보내주는 기본 500 에러는 절대 나와선 안된다. 모든 경우를 예외처리 해주고 신경써줘야 한다.
피드백 문제 해결
2023-06-10 의 회고