일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 프로그래머스 레벨1
- sql 데이터형 변환
- replaceAll()
- 모던자바
- toLowerCase()
- StringBuilder
- 래퍼타입
- 베주계수
- 자바 유클리드
- while과 two-pointer
- 스프링환경설정
- 스프링뼈대
- Git사용법
- git 컨벤션
- 최대공약수
- ineer join
- addDoc
- islowercase()
- stringbuilder의 reverse()
- 자바 최대공약수
- 자바 최소공배수
- 동일성과 동등성
- string과 stringbuilder
- 최대공약수와 최소공배수
- string
- 스프링
- 최소공배수
- 자바 스트링
- isuppercase()
- 유클리드호제법
- Today
- Total
주노 님의 블로그
240819 ~ 240830 숙련주차 개인과제 본문
일단 1~4단계의 erd를 보자.
일정과 댓글이 있고 일정 1 댓글 다 로 댓글에 일정의 외래키가 있다.
@ManyToOne
@JoinColumn(name = "task_id")
private Task task;
1단계
entity service repository dto와
auditing을 위한 timestamped 클래스를 만들었다
@OneToMany(mappedBy = "task")
private List<Reply> replyList = new ArrayList<>();
또한 연관관계설계도 위처럼 하였다
cru >> d는 3단계
2단계
댓글은 일정의 id를 받아서 crud를 구현하는것이다
그리고 자꾸
[nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: No acceptable representation]
이런 오류가 뜨는것이다
그리고 작성하다 깜빡했는지 dto에 게터 어노테이션을 적용하지 않은 문제!
감사합니다.. 구글님...
3단계
페이지네이션을 위해 Pageable과 Sort인터페이스를 사용하였다
public List<TaskResponseDto> getTasks(int page, int size)
{
Sort sort = Sort.by(Sort.Direction.DESC, "modifiedAt");
Pageable pageable = PageRequest.of(page-1 ,size, sort);
Page<Task> tasks = taskRepository.findAll(pageable);
List<TaskResponseDto> taskResponseDtos = new ArrayList<>();
for(Task task : tasks.getContent())
{
taskResponseDtos.add(new TaskResponseDto(task));
}
if(taskResponseDtos.isEmpty())
{
new IllegalArgumentException("일정 목록이 없습니다.");
}
return taskResponseDtos;
}
page와 size를 리퀘스트파람으로 받아서 페이지와 사이즈를 정한다
그리고 페이지가 0부터 시작한다고 며칠전 튜터님께 들었던 기억이났다
그래서 바로.. 줍줍 하였다.
jdbcTemplate와 비교하여 엄청 편해졌다
물론 jdbcTemplate도 어려운건 아니었지만 귀찮은 코드긴 했었다.
하지만 jpa를 사용함으로써 엄청 간편해지고 가독성이 좋은 코드로 바뀐것같다고 생각한다.
참고자료
https://zumsim.tistory.com/59
4단계
일정 전체 삭제.
나는 진짜 삭제가아닌 삭제된것처럼.. 보이게하는 boolean 변수를 이용하여 일괄 수정을 진행한다
@OneToMany(mappedBy = "task", cascade = CascadeType.PERSIST)
private List<Reply> replyList = new ArrayList<>();
cascade 타입을 PERSIST로 정한다.
사실 REMOVE가 맞는데 진짜 REMOVE는 사용을 못하니
근디 cascade 타입을 persist로 지정해도 delete를 수행하는건 수동인데...
softDelete는 이걸 구현할 수 있나?
힘들거같아서 branch를 따로 파서 작업하기로 했다.
진짜 삭제를 원할경우 delete로 하면된다
옛날에 jpa프로젝트할때 그걸로 교수님한테 피드백 받은적 있걸랑
@Transactional
public void deleteTask(Long id)
{
Task task = findTask(id);
if(task.isDeleteStatus())
{
throw new IllegalArgumentException("이미 삭제된 일정은 다시 삭제할 수 없습니다");
}
task.delete();
for(Reply reply : task.getReplyList())
{
reply.delete();
}
}
그리고 위처럼 구현해준다
트랜잭션을 걸어줘야 상태 변경이 이루어진다.
+실패시 롤백이 될수도 있으니..!
5단계
엄청난 코드 변경과 수정을 요하는 5단계
차라리 처음부터 다시짜는게 나을정도지만..
이리저리 수정해줘야한다
유저 엔티티를 추가해주면서
중간 테이블인
매니저 테이블을 추가해주고
유저와 매니저 일정간의 관계를 수정
일정 엔티티를 수정해주고
dto를 각각의 연관관계에 맞게 수정해주고.(일정을 추가하면 그에 맞게 회원 id랑 이어줘야함)
service는 삭제 로직만 바꿔주면된다
단계6
일정 단건 조회시 담당 유저 정보가 떠야한다면
private List<MemberResponseDto> managers = new ArrayList<>();
public TaskResponseDto(Task task)
{
this.id = task.getId();
this.title = task.getTitle();
this.contents = task.getContents();
this.registerAt = task.getRegisterAt();
this.modifiedAt = task.getModifiedAt();
this.delete_status = task.isDeleteStatus();
this.memberId=task.getMember().getId();
this.managers = task.getManagerList().stream()
.map(manager -> new MemberResponseDto(manager.getMember()))
.collect(Collectors.toList());
}
taskResponseDto를 수정한다
managers 정보를 위해 list에서 member정보를 가져와야하고
멤버 정보를 불러온다.
그러면 매니저들의 정보를 불러 올 수 있게된다
잠깐.
dto를 따로 빼야할것같다.
@Getter
@NoArgsConstructor
public class MemberManagerResponseDto
{
private Long id;
private String name;
private String email;
public MemberManagerResponseDto(Member member)
{
this.id =member.getId();
this.name =member.getName();
this.email =member.getEmail();
}
}
필요한 정보만을 담은 dto로 변경했다 간단하게 id name email만 담은 정보면 된다
일정 전체 조회시 지연로딩을 이용하라는디
package com.sparta.springadvancedpersonalproject.entity;
import com.sparta.springadvancedpersonalproject.dto.request.TaskCreateRequestDto;
import com.sparta.springadvancedpersonalproject.dto.request.TaskUpdateRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@NoArgsConstructor
@Table(name = "task")
public class Task extends Timestamped
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String title;
@Column(nullable = false, length = 200)
private String contents;
@Column
private boolean deleteStatus = false;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "task")
private List<Reply> replyList = new ArrayList<>();
@OneToMany(mappedBy = "task")
private List<Manager> managerList = new ArrayList<>();
public Task(TaskCreateRequestDto taskCreateRequestDto)
{
this.title = taskCreateRequestDto.getTitle();
this.contents = taskCreateRequestDto.getContents();
}
public Task(String title, String contents, Member member) {
this.title = title;
this.contents = contents;
this.member = member;
}
public void update(TaskUpdateRequestDto taskUpdateRequestDto)
{
this.title = taskUpdateRequestDto.getTitle();
this.contents = taskUpdateRequestDto.getContents();
}
public void delete()
{
this.deleteStatus = true;
}
}
단계 7
유저에 비밀번호를 추가하였다
그에 맞는 dto를 수정하였다
어떻게 비밀번호 암호화를 해야할까?
@Component
public class PasswordEncoder {
public String encode(String rawPassword) {
return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
}
public boolean matches(String rawPassword, String encodedPassword) {
BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
return result.verified;
}
}
일단 password encoder가 주어졌다
로우 패스워드를 encode하면 암호화가되어서 나타남
/**
* 회원을 등록합
* @param memberCreateRequestDto 회원 이름과 이메일을 받습니다
* @return 회원 아이디와 회원이름, 회원이메일, 삭제여부, 등록일자, 수정일자를 반환합니다.
*/
@PostMapping("/member")
public ResponseEntity<MemberResponseDto> signupMember(@RequestBody MemberCreateRequestDto memberCreateRequestDto, HttpServletResponse res) {
MemberResponseDto responseDto = memberService.signupMember(memberCreateRequestDto, res);
return new ResponseEntity<>(responseDto, HttpStatus.CREATED);
}
원래는 createMember이었지만 상황에 맞게 signup으로 변환하였다
또 쿠키에 사용될 수 있게 HttpServletResponse을 받는다
아참 튜터님이 단축어 쓰지말랫는데
public MemberResponseDto signupMember(MemberCreateRequestDto memberCreateRequestDto, HttpServletResponse response)
{
String password = passwordEncoder.encode(memberCreateRequestDto.getPassword());
Member member = new Member(memberCreateRequestDto.getName(), password, memberCreateRequestDto.getEmail());
memberRepository.save(member);
String token = jwtUtil.createToken(member.getId());
jwtUtil.addJwtToCookie(token, response);
MemberResponseDto responseDto = new MemberResponseDto(member);
return responseDto;
}
급하게 response로 바꾸고
위 코드에서 password를 받은 비밀번호를 넣어 인코딩한다
당연 저건 매치로 로그인할때 쓰면될듯
그리고 멤버를 만들때는 암호화된 암호를 넣어주고
토큰을 받아주기위해 jwtUtil에 있는 createToken으로 멤버아이디를 넣었다
util은 아직은 유저 권한이 필요없어서 권한부분은 자름
그러니까 담아서 주긴했다
어차피 bearer이후로 잘릴거니까 받아올수 있긴함!
패스워드도 암호처럼 들어간것을 볼 수 있다
추가 공부한 자료
https://brunch.co.kr/@jinyoungchoi95/1
단계 8
아까 받은 암호화된 비밀번호와 입력된 비밀번호를 매치시켜서
맞으면 로그인하는 기능을 구현해보자.
@Column(name = "email", unique = true, nullable = false, length = 100)
private String email;
일단 이메일로 로그인하니 unique를 ture로 체크해서 중복이 안되게 하자
테이블을 삭제하고 하이버네이트가 다시 만들게 하자
/**
* 회원 로그인을 진행합니다.
* @param memberLoginRequestDto 로그인정보는 이메일과
* @param response
* @return 반환값은 jwt값입니다.
*/
@PostMapping("/member/login")
public String loginMember(@RequestBody MemberLoginRequestDto memberLoginRequestDto, HttpServletResponse response)
{
String token = memberService.loginMember(memberLoginRequestDto, response);
return token;
}
controller에 로그인을 만들어주자
로그인 dto는 아이디와 이메일만을 받으니 따로 dto를 만들어주자
public String loginMember(MemberLoginRequestDto memberLoginRequestDto, HttpServletResponse response)
{
String email = memberLoginRequestDto.getEmail();
String password = memberLoginRequestDto.getPassword();
Member member = memberRepository.findByEmail(email);
if(member == null)
{
throw new IllegalArgumentException("회원이 존재하지 않습니다");
}
if(!passwordEncoder.matches(password, member.getPassword()))
{
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다");
}
String token = jwtUtil.createToken(member.getId());
jwtUtil.addJwtToCookie(token, response);
return token;
}
회원이 없을때와 비밀번호가 맞을때를 확인하는 로직을 추가하고
로그인이 성공되었을때 토큰을 만들고 받은 헤더에넣어준다.
이렇게 전송하면된다.
만약 토큰 시간이 이렇게 초과되면 에러를 반환한다.
JWT expired at 2024-08-28T19:43:44Z. Current time: 2024-08-29T01:49:01Z, a difference of 21917534 milliseconds. Allowed clock skew: 0 milliseconds.
만약 토큰이 올바르지않으면 아래와같은 에러를 띄운다
Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.
jwt bearer를 통해 전송하는방법
encoded에 받은 jwt토큰을 넣으면
payload
저렇게 넣어주면된다
필터가 토큰이 없음을 확인하고 잡아줬다
이제 모든 단계에서
위 링크에서는 토큰이 없어도 검증이 되지않지만
다른곳에서는 토큰이없으면 안된다
그리고 이메일과 비밀번호가 다를때 responseEntity를 사용하여
상태코드를 같이 반환해보자
그리고 토큰이 없을때의 로직은 모든 서비스에 구현하기 어렵다
아니 귀찮다
그래서 우리가 아까 사용했던 authFilter에 사용해보자
if (!jwtUtil.validateToken(token)) {
throw new IllegalArgumentException("Token Error");
}
위 코드긴 한데.. 문제는 void이다.
갑자기 생각난 내용은
status를 반환한 후에
return으로 강종시키는 방법이..
인텔리제이가 편해진걸 느낀 나
참고자료
https://velog.io/@2jjong/Spring-Boot-s6xmqo77
과제9
권한 인가를 만들기 위해 user에 권한을 구분 해 줄 수 있는
엔티티에 권한을 enum으로 넣었다
과제10
엄청난 고민
일단 restTemplate는 이번 과제로 거의 처음 들어봤다
(다른것도 사실 똑같다)
일단 코드스니펫을 옮겨보았다.
public List<TaskWeatherDto> getWeather()
{
URI uri = UriComponentsBuilder
.fromUriString("대충링크")
.path("대충링크")
.encode()
.build()
.toUri();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
return fromJSONtoItems(responseEntity.getBody());
}
대충 저런링크를 타고 들어가서 받은 파일을 받아서 날씨를 업로드해야한다
구렇다
이거 강의 코드 약간만 수정하면 되겠는데! 싶었지만... 마음만큼은 그렇게 되지 않았다..
대충 조회라도 해보려고 get에 넣고 돌려보았다
A JSONObject text must begin with '{' at 1 [character 2 line 1]
그래도 정상적으로 불러는 오나,
JsonObject형식은 { 이건데 넌 왜 [ 이걸썻냐 이말인거같다
https://jinsangjin.tistory.com/15#google_vignette
그래서 보니 JsonArray도 있길래 array를 사용하기로 했다
for(int i = 0 ; i<weathers.length() ; i++)
{
JSONObject jsonObject = weathers.getJSONObject(i);
if(jsonObject.get("date").equals("03-25"))
{
System.out.println("이거마따");
}
}
그래서 일단 테스트를 해봤다
당연히 뱉었다.
이 내용을 사용해서 쓰면될것같았다
음... 다만 for문을 계속 순회한다는게 엄청난 비효율이긴한디
저 api를 저장하고 쓰는게 낫긴한데 일단 요구사항만 지키자!
일단 내생각엔
task에 weather을 만들고
service에서 날짜와 맞는 weather만 받아오면 된다!
public List<TaskWeatherDto> fromJSONtoItems(String responseEntity) {
JSONArray weathers = new JSONArray(responseEntity);
List<TaskWeatherDto> taskWeatherDtos = new ArrayList<>();
for(int i = 0 ; i < weathers.length(); i++)
{
JSONObject object = weathers.getJSONObject(i);
taskWeatherDtos.add(new TaskWeatherDto(object));
}
return taskWeatherDtos;
}
일단 task를 list에 넣었다.
날짜랑 매칭되는 것만 받아오면 될것같다.
private String findTodayWeather(List<TaskWeatherDto> taskWeatherDtos)
{
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("MM-dd");
String date = LocalDate.now().format(dateFormatter);
String weather = "";
for(TaskWeatherDto taskWeatherDto : taskWeatherDtos)
{
if(taskWeatherDto.getDate().equals(date))
{
weather = taskWeatherDto.getWeather();
}
}
return weather;
}
생각해보니 dto에서 등록되는 날짜를 찾지 않아되 될거같아서
그리고 findByWeather메서드를 만들어서
오늘 날짜를 입력받아서 매칭되는 날짜를 찾는다
참고자료
https://kim-jong-hyun.tistory.com/32
리팩토링
1. 과제를 제출하기전 N+1문제를 확인하기위해 확인해보았다.
2. 과제4 - 영속성 전이를 위해 hard delete로 바꿨다
cascade 타입을 remove로 하여서
멤버가 삭제되면 주루룩 삭제 되게 하였고
@OneToMany(mappedBy = "task", cascade = CascadeType.REMOVE)
private List<Reply> replyList = new ArrayList<>();
task도 삭제되면 reply가 삭제되도록 구성하였다
task가 삭제되면 reply도 삭제되는것을 볼 수 있다.
3. 10단계
기존에 돌던 코드는 TASK를 만드는 과정에서
서버에서 받아온 파일 DTO에 저장 (365회)
그다음 오늘 날짜를 자바에서 설정해서 오늘 날짜에 맞는 LIST를 조회한다.
즉 12월 31일은 TASK를 등록하는데 365 +365 거의 700번을 저기서 돌게된다
O(N)으로 작동하지만 솔직히 비효율적인거 같은느낌이..
그래서 이리저리 묻고 다녔다
팀원분의 의견에는 MAP으로 하라 였고, 다른분은 이분탐색을 하라였다
둘다 좋은 구현이었지만 MAP으로 구현하기로 했다.
또 생성자에다가 서버에서 데이터를 불러오면 한번만 실행되니 좋지않을까 생각했다
기존 버전에서 걸린시간 552ms
개선된 버전에서 걸린 시간 52ms
10배나 개선된 시간을 얻을 수 있었다.
추가공부
3시간 공부한것의 결과는 아래와같이 간단했다 ㅇㅅㅇ...
exceptionHandler
@ControllerAdvice / @ExceptionHandler
스프링에서 특정 예외가 발생하였을때 호출되는 메서드를 정의하는것이다
컨트롤러 내에서 작동하며 하위 메서드에도 적용이된다
모든 컨트롤러에 일괄 적용하기위해 @ControllerAdvice를 적용한다
responseEntity
응답 본문과 상태코드를 던져줄수 있다
return new ResponseEntity<>(responseDto, HttpStatus.OK);
두개의 조합!
//401에러
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException exception)
{
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(exception.getMessage());
}
.
IllegalArgumentException을 던진다면 401에러와 함께 body에 메세지를 보내준다
public class NoAuthorityException extends RuntimeException
{
public NoAuthorityException() {
super("권한이 없습니다.");
}
}
커스텀에러 물론 만들어서
//권한이 없다면 403을 반환해야함.
@ExceptionHandler(NoAuthorityException.class)
public ResponseEntity<String> handleAccessDeiedException(NoAuthorityException noAuthorityException)
{
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("권한이 없습니다");
}
과제 후기
0단계 ~ 10단계 중 몇 단계까지 과제를 완성했나요?
10
[참고] 브랜치가 나뉘어져 있습니다
main브랜치는 soft delete를 구현하여 4단계 과제(영속성 전이) 가 빠져있습니다
feature/hrdDelete브랜치는 repository의 delete를 사용하여 영속성 전이 기능을 넣었습니다.
기술 질문 1. 처음 설계한 API 명세서에 변경 사항이 있었나요? 변경 되었다면 어떤 점 때문 일까요?
변경이 되었습니다.
일단 api설계와 erd가 파트별로 4개가 되어 깊게 생각하지 않은채 작성을 하였습니다 추후 수정작업을 거쳤습니다
기술 질문 2. ERD를 먼저 설계한 후 Entity를 개발했을 때 어떤 점이 도움이 되셨나요?
필드를 어떤것을 작성해야할지 미리 알고있으니 작성의 편리함이 있습니다 연관관계도 erd매핑에따라 처리를하면됩니다.
기술 질문 3. JWT를 사용하여 인증/인가를 구현 했을 때의 장점은 무엇일까요?
stateless에 걸맞게 서버는 클라이언트의 상태를 가지고 있지 않습니다 또한 토큰을 검증하기때문에 암호화된 정보를 확인 할 수 있습니다 또한 확장성이라고 생각합니다. 7단계를 할때는 권한을 넣지않았지만 9단계에서 권한 기능을 동적으로 추가를 하여도 코드 한줄로만 정보를 담을수 있다고 생각했습니다.
단점이라면 서버는 단지 검증만 하기때문에, jwt를 탈취 당하는 보안이 떨어진다고 생각됩니다.
기술 질문 4. 만약 댓글이 여러 개 달려있는 할일을 삭제하려고 한다면 무슨 문제가 발생할까요?
cascade를 사용하지 않았다면 외래키 오류가 뜰것입니다.
cascade.REMOVE나 orphanremoval = true로 작성을하면 고아객체는 삭제될것이고, 부모가 삭제되면 자식도 전파되어 삭제될것입니다
기술 질문 5. 연관관계를 설정할 때 단방향과 양방향으로 맺는 것의 차이점은 무엇일까요? 각 방법의 장단점은 무엇이 있을까요?
단방향 관계 :
코드가 단순해지며, 한쪽만 참조할 경우에 사용할수 있습니다.
단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 되어있습니다.
다만, 한쪽만 참조를 할 수 있습니다.
양방향 관계 :
양쪽 객체를 서로 참조할수 있습니다.
다만 복잡성이 증가합니다, 단방향 매핑후, 필요할경우 양방향 매핑을 하는것이 좋습니다.
과제를 하며 어려웠던 점은 무엇이었나요?
7~10은 처음 접하는 것이기도 하고
코드스니펫을 참조 하기도 하였습니다
조금씩 수정을 하며 이해를 하기는 했지만, 온전히 제가 짰다고는 할 수 없습니다.
만약 코드스니펫과 강의가 없었다면 6에서 제출을 끝내지 않았을까.. .싶기도...
배운점
저번 피드백에서 받은 exception 핸들러와 response entity에 대해서 확실하게 깨우치게 되었고
jwt와 filter에 대해 코드를 수정해보며 어떤 흐름으로 진행되는지 조금 이해하게 되었다
과제를 하면서 cascade와 orphanremoval에 대해 생각했고
데이터베이스 정규화에 관해서 (솔직히 말이 너무 어려웠음 원자성이나, 이행적 함수종속이나 ㅇㅇ..) 더 이해하는 시간이 되었다!
상태코드를 생성하며, 어떨때 적합한 상태코드를 반환해야하는지 느끼게 되었다
팀원들의 오류사항을 보고 쿼리의 실행 순서가 있다는것을 알았으며
복잡한 로직일때 flush를 적절히 사용해야 한다는것을 느꼈다.
아니면 jpql쓰던가..!
참고자료
https://lovon.tistory.com/95
느낀점
jpa를 사용하며 jdbc보다 편리해지기도 했고 쿼리문을 작성하지 않아도 되니 편했다
트랜잭션의 중요함을 알게되었다