Light Blue Pointer
본문 바로가기
Developing/개발일지

2023-12-06, Today I Leanred

by Greedy 2023. 12. 6.

오늘 한 일 

햇살마켓(유사 당근마켓 프로그램) 개발하면서 

게시글 CRUD를 하고 Authorization이 넘어오면 로그인된 user정보 이용해서 유저 확인 후 수행하기()

 

오늘 알게된 것

⭐ResponseDto에 Getter를 달지 않으면 객체가 Json으로 변환이 안 되는 것 같다

⭐HttpStatus.CREATED → HttpStatusCode 임

⭐HttpStatus.CREATED.value() → int임

⭐db에는 created_at으로 저장되더라도 Entity에는 createdAt으로 써야한다 그렇지 않으면 찾지 못함

⭐제너릭 타입으로 리턴타입을 감싸서 항상 공통적으로 덧붙여지는 정보를 전달해줄 수 있다

 

오늘 개선한 것

💡개선 : return 타입으로 commonresponse돌려보내고 싶어서 responsedto마다 다 상속받던걸 개선해보기

원래 리턴 타입을 ResponseEntity로 하고있었는데 상태메시지도 돌려보내고 싶어짐

repository

public interface ItemRepository extends JpaRepository<Item, Long> {
    List<Item> findAllByOrderByCreatedAtDesc();
}

service

public List<ItemResponseDto> getAllItems() {
        return itemRepository.findAllByOrderByCreatedAtDesc().stream()
                .map(ItemResponseDto::new)
                .collect(Collectors.toList());
    }

controller

//선택 상품 조회
    @GetMapping("/items")
    public ResponseEntity<ItemAllResponseDto> getAllItems(
            @RequestParam String type
    ) {
        try {
            ItemAllResponseDto responseDto = new ItemAllResponseDto();
            responseDto.setItemResponseDtos(itemService.getAllItems());
            return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return ResponseEntity.badRequest().body(new ItemAllResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));
        }
    }

 

저번 팀플에서는 리턴 타입을 두개로 할 수가 없으니 에러메시지/메시지와 HTTP상태코드번호를 가지는 CommonResponse객체를 만들어서 모든 Response객체에 상속시키고 모든 Response객체에 super(message,statuscode) 생성자를 만들어서 해결했었다

그런데 이번 팀플은 저번 팀플보다 볼륨이 커지고 Response객체도 많아지다 보니까 그런 식으로 하면 일이 엄청 늘어나고 ResponseDto들의 List를 중간에 전달할 일도 많았는데 모든 ResponseDto에 CommonResponse 필드가 다 달려다닌다고 생각하니까 너무 이상했다...

 

⛳해결 : 제너릭 리턴 객체 만들기

https://velog.io/@qotndus43/스프링-API-공통-응답-포맷-개발하기

이 블로그 참고했다!

에러 부분은 내일 좀 더 공부하고 Exception 커스터마이징도 하고싶다

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiResponse<T> {
    
    private String status;
    private String message;
    private T data;
    
    public ApiResponse(String stts, String msg, T data){
        this.status = stts;
        this.message = msg;
        this.data = data;  
    }
}

나는 원래 status code랑 message붙은 commonResponse 돌려보대고 싶은 거여서 저렇게 만들었다

EntityResponse여태까지 <>왜 괄호 안에다 타입 넣어서 쓰는지 궁금했는데 제너릭이었군!!

public class ResponseEntity<T> extends HttpEntity<T> {

	private final HttpStatusCode status;

	/**
	 * Create a {@code ResponseEntity} with a status code only.
	 * @param status the status code
	 */
	public ResponseEntity(HttpStatusCode status) {
		this(null, null, status);
	}

	/**
	 * Create a {@code ResponseEntity} with a body and status code.
	 * @param body the entity body
	 * @param status the status code
	 */
	public ResponseEntity(@Nullable T body, HttpStatusCode status) {
		this(body, null, status);
	}

	/**
	 * Create a {@code ResponseEntity} with headers and a status code.
	 * @param headers the entity headers
	 * @param status the status code
	 */
	public ResponseEntity(MultiValueMap<String, String> headers, HttpStatusCode status) {
		this(null, headers, status);
	}

	/**
	 * Create a {@code ResponseEntity} with a body, headers, and a raw status code.
	 * @param body the entity body
	 * @param headers the entity headers
	 * @param rawStatus the status code value
	 * @since 5.3.2
	 */
	public ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, int rawStatus) {
		this(body, headers, HttpStatusCode.valueOf(rawStatus));
	}

가서 보니까 엄청나게 많은 생성자와 빌더가 있었다

 

원래 ResponseEntity.status(HttpStatus.CREATED).body(responseDto); 이렇게 하고 있었는데

⭐HttpStatus.CREATED → HttpStatusCode 임

⭐HttpStatus.CREATED.value() → int임

 

@PostMapping("/add")
    public ApiResponse<ItemResponseDto> addItem(
            @RequestBody ItemRequestDto requestDto
    ) {
        try {
            ItemResponseDto responseDto = itemService.addItem(requestDto);
             ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
            return new ApiResponse<ItemResponseDto>(HttpStatus.CREATED.value(),"아이템 추가 성공했습니다",responseDto);
        }

이렇게 바꾸고

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiResponse<T> {

    private int status;
    private String message;
    private T data;

    public ApiResponse(int status, String message, T data){
        this.status = status;
        this.message = message;
        this.data = data;
    }
}

이렇게 만들었다

 

catch부분도 이렇게 바꾸고

catch (RejectedExecutionException | IllegalArgumentException ex){
            return new ApiResponse<ItemResponseDto>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }

ApiResponse에다가 메시지랑 int value(status code)만 있는 생성자 추가했다

선택상품 조회 기능도 이렇게 바꿈

//선택 상품 조회
    @GetMapping("/{itemId}")
    public ResponseEntity<ItemResponseDto> getItem(
            @PathVariable Long itemId
    ) {
        try {
            ItemResponseDto responseDto = itemService.getItem(itemId);
            return ResponseEntity.status(HttpStatus.OK).body(responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return ResponseEntity.badRequest().body(new ItemResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));

        }
    }

@GetMapping("/{itemId}")
    public ApiResponse<ItemResponseDto> getItem(
            @PathVariable Long itemId
    ) {
        try {
            ItemResponseDto responseDto = itemService.getItem(itemId);
            return new ApiResponse<ItemResponseDto>(HttpStatus.OK.value(),"아이템 조회에 성공했습니다",responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return new ApiResponse<ItemResponseDto>(HttpStatus.BAD_REQUEST.value(),ex.getMessage());
        }
    }

 

 

 

에러 해결

🚩문제 : 

 

2023-12-06T11:34:08.057+09:00 ERROR 17584 --- [ restartedMain] o.s.b.d.LoggingFailureAnalysisReporter :


APPLICATION FAILED TO START


Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class

Action:

Consider the following: If you want an embedded database (H2, HSQL or Derby), please put it on the classpath. If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

⛳해결  : application.properties가 비어있었던거 채워줌

spring.datasource.url=jdbc:mysql://localhost:3306/orderapp
spring.datasource.username=root
spring.datasource.password=58155815
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

저거 넣고나니 빌드 성공했다

//선택 상품 조회
    @GetMapping("/items")
    public ResponseEntity<ItemAllResponseDto> getItems(
            @RequestParam String type
    ) {
        if(type.equals("All")){ return getAllItems();}
        else if(type.equals("Myselect")){return ResponseEntity.badRequest().body(new ItemAllResponseDto("myselect", HttpStatus.BAD_REQUEST.value()));}
        else{return ResponseEntity.badRequest().body(new ItemAllResponseDto("올바르지 않은 요청입니다", HttpStatus.BAD_REQUEST.value()));}
    }

    public ResponseEntity<ItemAllResponseDto> getAllItems()
    {
        try {
            ItemAllResponseDto responseDto = new ItemAllResponseDto();
            responseDto.setItemResponseDtos(itemService.getAllItems());
            return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return ResponseEntity.badRequest().body(new ItemAllResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));
        }
    }

프론트단 이렇게 바꿔봄

 

🚩문제 : 빈 객체가 옴

localhost:8080/api/items/get?type=All

보내니까

{}

이게 옴

빈 객체가 온거같음

Hibernate: 
    /* <criteria> */ select
        i1_0.id,
        i1_0.address,
        i1_0.completed,
        i1_0.content,
        i1_0.created_at,
        i1_0.delivered,
        i1_0.image,
        i1_0.modified_at,
        i1_0.price,
        i1_0.seller_id,
        i1_0.title 
    from
        items i1_0 
    order by
        i1_0.created_at desc

responseDto.setItemResponseDtos(itemService.getAllItems());

이부분이 안 되고 있는 거 같음

@Setter
@RequiredArgsConstructor
public class ItemAllResponseDto extends CommonResponseDto {
    List<ItemResponseDto> itemResponseDtos;

Setter가 안 되는건가 싶어서 Setter를 만들어봄

public void setItemResponseDtos(List<ItemResponseDto> itemResponseDtos){
        this.itemResponseDtos = itemResponseDtos;
    }
{}

또 저게 옴

읽지 못하는게 Getter가 없어서일 수도 있다고 생각해서

⛳해결 : Getter를 달아봄

⭐Getter를 달지 않으면 객체가 Json으로 변환이 안 되는 것 같다

@Getter
@Setter
@RequiredArgsConstructor
public class ItemAllResponseDto extends CommonResponseDto {
    List<ItemResponseDto> itemResponseDtos;

    public ItemAllResponseDto(String msg, Integer statuscode){
        super(msg,statuscode);
    }
}
{
    "itemResponseDtos": []
}

Getter를 달았더니 이건 나옴 비어있지만

그래서 ItemResponseDto에도 Getter달아봄

{
    "itemResponseDtos": []
}

여전히 이렇게 뜬다

findAll()

Repository findAll해봄

여전히 그렇게 뜸

return itemRepository.findAllByOrderByCreatedAtDesc().stream()
                .map(ItemResponseDto::new)
                .collect(Collectors.toList());

이부분이 뭔가 잘못됐나?

생성자로 responseDto만들수 있어야 하나 근데 있는데

public ResponseEntity<ItemAllResponseDto> getAllItems()
    {
        try {
            ItemAllResponseDto responseDto = new ItemAllResponseDto();
            responseDto.setItemResponseDtos(itemService.getAllItems());
            return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return ResponseEntity.badRequest().body(new ItemAllResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));
        }
    }

일단 이걸 바꿔봄

제대로 오나 보게

@GetMapping("/get")
    public ResponseEntity<List<ItemResponseDto>> getItems(
            @RequestParam String type
    ) {
        if(type.equals("All")){ return getAllItems();}
        //else if(type.equals("Myselect")){return ResponseEntity.badRequest().body(new ItemAllResponseDto("myselect", HttpStatus.BAD_REQUEST.value()));}
        //else{return ResponseEntity.badRequest().body(new ItemAllResponseDto("올바르지 않은 요청입니다", HttpStatus.BAD_REQUEST.value()));}
        return null;
    }

    public ResponseEntity<List<ItemResponseDto>> getAllItems()
    {
//        try {
//            ItemAllResponseDto responseDto = new ItemAllResponseDto();
//            responseDto.setItemResponseDtos(itemService.getAllItems());
            return ResponseEntity.status(HttpStatus.CREATED).body(itemService.getAllItems());
//        }catch (RejectedExecutionException | IllegalArgumentException ex){
//            return ResponseEntity.badRequest().body(new ItemAllResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));
//        }
    }
[]

이렇게 옴 안 찾아지는게 맞는거같음

 

⛳ 해결 : application.properties에 db이름 잘못 쓴 거 고침

spring.datasource.url=jdbc:mysql://localhost:3306/orderapp

 

ㅎ혹시나 해서 db연결 확인해보니 이걸 잘못써놨음

spring.datasource.url=jdbc:mysql://localhost:3306/sunlightmarket

이렇게 수정함 

{
    "itemResponseDtos": [
        {
            "seller_id": 1,
            "title": "제목제목",
            "image": "src",
            "price": "2,000",
            "address": "내머릿속",
            "content": "내용내용",
            "created_at": "2023-12-05T22:23:35.837991",
            "modified_at": "2023-12-05T22:23:35.837991"
        },
        {
            "seller_id": 1,
            "title": "제목제목",
            "created_at": "2023-12-05T22:19:30.19611",
            "modified_at": "2023-12-05T22:19:30.19611"
        }
    ]
}

잘 뜸

 

Get item 기능 개발

Controller

@GetMapping("/{itemId}")
    public ResponseEntity<ItemResponseDto> getItem(
            @PathVariable Long itemId
    ) {
        try {
            ItemResponseDto responseDto = itemService.getItem(itemId);
            return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
        }catch (RejectedExecutionException | IllegalArgumentException ex){
            return ResponseEntity.badRequest().body(new ItemResponseDto(ex.getMessage(), HttpStatus.BAD_REQUEST.value()));

        }
    }

Service

public ItemResponseDto getItem(Long itemId){
        Item item = itemRepository.findById(itemId).orElse(null);
        return new ItemResponseDto(item);
    }

Repository

Optional<Item> findById(Long itemId);

처음에는 repository에서 그냥 Item findByID(Long itemId);

하고싶었는데 빨간줄이 뜨면서 Optional로 만들라고 함

Q. 왜? →

근데 저번에는 그냥 Item처럼 만들었었는데 …

Creating an Object from Optional<Object>

findFirst() gives you an Optional and you then have to decide what to do if it's not present. So findFirst().orElse(null) should give you the object or null if it's not present

<Optional>Item으로 오는거 처리하기

Item item = itemRepository.findById(itemId).orElse(null);

잘 뜸

나중에 저 코드는

Item item = itemRepository.findById(itemId).orElseThrow(NullPointerException::new);

이렇게 수정함

 

{
    "seller_id": 1,
    "title": "제목제목",
    "image": "src",
    "price": "2,000",
    "address": "내머릿속",
    "content": "내용내용",
    "created_at": "2023-12-05T22:23:35.837991",
    "modified_at": "2023-12-05T22:23:35.837991"
}

 

 

 

 

🚩문제 : 그러고 실행시켰는데 자꾸 autogenerated password뜨고 401 unauthorized라고 응답이 옴

⛳해결 : build.gradle에 security 주석처리한거 빌드 다시했더니 해결됨

 

🚩문제  : 팀원한테 kakao_id 필드를 찾을수 없다는 에러가 자꾸 떴음

⛳해결  : kakao_id → kakaoId

로 했더니 됨

private LocalDateTime createdAt;

⭐db에는 created_at이지만 entity에는 _넣으면 안되고 createdAt을 넣어야 함

 

 

🚩문제 : Update가 안 됨

localhost:8080/api/items/4

update를 하면 403forbidden이 뜸…

@PutMapping("/items/{itemsId}")

put을 patch로 바꿔봄

그래도 또 뜸 403 forbidden이

private Item getUserItem(Long itemId, User user){
        Item item = itemRepository.findById(itemId).orElseThrow(NullPointerException::new);
//        if(!item.getUser().getId().equals(user.getId())){
//            throw new RejectedExecutionException("작성자만 수정할 수 있습니다.");
//        }
        return item;
    }

이부분 주석처리하고 테스트해봄

그래도 403으로 안됨

Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'PUT' is not supported]

이는 해당 HttpMethod를 mapping하는 Controller가 존재하지 않아서 발생한 에러였다. 컨트롤러에 PUT으로 받는 메소드를 추가해 문제를 해결할 수 있었다.

근데 나는 버젓이 있는데요 ??

@PutMapping("/items/{itemsId}")

아 주소가 틀려도 저렇게 나오는 거였음

items를 또 하면 안됨

@PutMapping("/{itemsId}")

이렇게 수정함

 

Resolved [org.springframework.web.bind.MissingPathVariableException: Required URI template variable 'itemId' for method parameter type Long is not present]

ㅋㅋ

localhost:8080/api/items/1 으로 get 하는것도 안 되기 시작함 ㅋㅋㅋㅋㅋㅋ

조회는 왜 되고 update는 왜 path를 못 찾지?

Resolved [org.springframework.web.bind.MissingPathVariableException: Required URI template variable 'itemId' for method parameter type Long is not present]

Spring MVC Missing URI template variable

@PathVariable is used to tell Spring that part of the URI path is a value you want passed to your method. Is this what you want, or are the variables supposed to be form data posted to the URI?

If you want form data, use @RequestParam instead of @PathVariable.

If you want @PathVariable, you need to specify placeholders in the @RequestMapping entry to tell Spring where the path variables fit in the URI. For example, if you want to extract a path variable called contentId, you would use:

@RequestMapping(value = "/whatever/{contentId}", method = RequestMethod.POST)

 

⛳해결 : @RequestParam으로 바꾸니까 됨

Q.대체 왜???

localhost:8080/api/items?id=1

 

 

 

🚩문제 : 게시글 조회시 로그인을 안 하면 조회가 안 됨

로그인 정보가 없어도 게시글 조회가 되게 하고싶음

⛳해결 : WebSecurityConfig에서 인증 안 거쳐도 갈 수 있도록 path 걸러줌

httpSecurity.authorizeHttpRequests(
            (authorizeHttpRequests) -> authorizeHttpRequests
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .requestMatchers("/").permitAll()
                .requestMatchers("/api/users/**").permitAll()
                    **.requestMatchers("/api/items/read/**").permitAll()**
                .anyRequest().authenticated());

items/read로 시작하는 parh로 오는 모든 요청은 인증처리 안 하게 함

 

WebSecurityConfig

package com.raincloud.sunlightmarket.global.config;

import com.raincloud.sunlightmarket.global.jwt.JwtUtil;
import com.raincloud.sunlightmarket.global.security.JwtAuthenticationFilter;
import com.raincloud.sunlightmarket.global.security.JwtAuthorizationFilter;
import com.raincloud.sunlightmarket.global.security.UserDetailsServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    private final AuthenticationConfiguration configuration;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
        throws Exception {

        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
        filter.setAuthenticationManager(authenticationManager(configuration));
        return filter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthentizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf((csrf) -> csrf.disable());

        httpSecurity.sessionManagement(
            (sessionManagemnet) -> sessionManagemnet
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        httpSecurity.authorizeHttpRequests(
            (authorizeHttpRequests) -> authorizeHttpRequests
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .requestMatchers("/").permitAll()
                .requestMatchers("/api/users/**").permitAll()
                    .requestMatchers("/api/items/read/**").permitAll()
                .anyRequest().authenticated());

        httpSecurity
            .addFilterBefore(jwtAuthentizationFilter(), JwtAuthenticationFilter.class);
        httpSecurity
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }

}

 

 

 

'Developing > 개발일지' 카테고리의 다른 글

2023-12-08, Today I Learned  (1) 2023.12.08
2023-12-07, Today I Leanred  (1) 2023.12.07
2023-12-05, Today I Learned  (0) 2023.12.05
2023-12-04, Today I Learned  (0) 2023.12.04
2023-11-30, Today I Learned  (0) 2023.11.30