기존에는 직접 Dto나 Parameter을 통해 유저의 Id를 받아오는 방식사용하였지만

 

Security를 적용한 이후 헤더에 입력된 토큰값을 통해서  해당 유저의 로그인 아이디를 받아올 수 있다.

- SecurityContextHolder.getContext().getAuthentication().getName()

 

 

아래 메서드를 통해 헤더값에 넣어주는 토큰을 통하여

토큰을 발급받아 사용하는 유저가 로그인시 입력한 이메일 정보를 얻어오고

이메일 정보를 이용해서 해당유저의 엔티티를 가져올 수 있는 코드이다. 

 

원하는 곳에 추가하여 사용하면 된다.

private Member loginMemberFindByToken(){
    String loginEmail = SecurityContextHolder.getContext().getAuthentication().getName(); // 토큰에서 유저 email 확인
    return memberService.findMemberByEmail(loginEmail); //이메일을 통해 유저 엔티티 반환
}

 

 

 

필요한 코드 -

Service 

public Member findMemberByEmail(String email){ //로그인 이메일을 통해 유저 엔티티 반환 메서드
    Optional<Member> foundMember = memberRepository.findByEmail(email);
    return foundMember.orElseThrow(()-> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
}

 

Repository - 이메일에 대한 회원정보 찾기

public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByEmail(String email);
}

 

네이버 검색 API연동해서 뉴스 크롤링.

 

https://developers.naver.com/docs/serviceapi/search/news/news.md#%EB%89%B4%EC%8A%A4

 

검색 > 뉴스 - Search API

검색 > 뉴스 뉴스 검색 개요 개요 검색 API와 뉴스 검색 개요 검색 API는 네이버 검색 결과를 뉴스, 백과사전, 블로그, 쇼핑, 영화, 웹 문서, 전문정보, 지식iN, 책, 카페글 등 분야별로 볼 수 있는 API

developers.naver.com

예시까지 친절하게 설명해준다.

 

먼저 내 애플리케이션을 등록하고 API에 대한 id와 key를 받아야한다.

 

나는 400/401에러가 계속났는데 yml의 id와 key를 잘못 입력하고 원인을 다른곳에서 찾아 몇시간을 헤맸다.

 

 

yml 설정부

naver:  #naver API 설정
  url:
    search:
      news: https://openapi.naver.com/v1/search/news.json   #뉴스검색 URL
      image: https://openapi.naver.com/v1/search/image    #이미지검색
  client:
    id: ENC(rbiCFlHAp11Uusawse9EDX4NTmpRvfEJewqabaeeqefvZM=) #암호화된 키
    secret: ENC(CbqkdospKojXkFsiY2OueQOdLpMbveCTJkbKN9/)

 

Dto

public class SearchNewsDto {
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Post { //검색 쿼리
        private String query = ""; //검색어. UTF-8로 인코딩되어야 합니다.
        private int display = 20; //한 번에 표시할 검색 결과 개수(기본값: 10, 최댓값: 100)
        private int start = 1; //검색 시작 위치(기본값: 1, 최댓값: 1000)
        private String sort = "sim"; // sim: 정확도순 , date: 날짜순

        /*데이터를 한꺼번에 들어가게 해주는 메서드*/
        public MultiValueMap<String, String> toMultiValueMap() {
            var map = new LinkedMultiValueMap<String, String>();

            map.add("query", query);
            map.add("display", String.valueOf(display));
            map.add("start", String.valueOf(start));
            map.add("sort", sort);

            return map;
        }
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Response { //반환값
        private String lastBuildDate; //검색한 결과를 생성한 시간
        private int total; // 총 검색결과 갯수
        private int start; // 검색 시작 위치
        private int display; // 한번에 표시할 검색결과 갯수
        private List<NewsItem> items;
        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        public static class NewsItem {
            private String title; //제목
            private String originallink; // 뉴스기사 원문 URL
            private String link; //뉴스 기사의 네이버 뉴스 URL. 네이버에 제공되지 않은 기사라면 기사 원문의 URL을 반환합니다.
            private String description; // 뉴스기사 내용을 요약한 정보
            private String pubDate;
        }
    }
}

 

Controller

@RequiredArgsConstructor
@RequestMapping("/news")
@RestController
public class NaverSearchController {
    private final NaverClient naverClient;
    @GetMapping
    public ResponseEntity searchNews(@RequestParam(required = false, defaultValue = "환경") String query,
                                     @RequestParam(required = false, defaultValue = "1") int start,
                                     @RequestParam(required = false, defaultValue = "20") int display,
                                     @RequestParam(required = false, defaultValue = "sim") String sort){

         SearchNewsDto.Response response = naverClient.searchQuerySet(query, start, display, sort);

        return new ResponseEntity<>(response, HttpStatus.OK);
    }
}

 

Service

@Service
public class NaverClient {
    @Value("${naver.client.id}")
    private String naverClientId; //id
    @Value("${naver.client.secret}")
    private String naverClientSecret; //시크릿키
    @Value("${naver.url.search.news}")
    private String naverNewSearchUrl; //뉴스 검색주소
    @Value("${naver.url.search.image}")
    private String naverImageSearchUrl; //이미지 검색주소

    /* 뉴스검색 메서드*/
    public SearchNewsDto.Response searchNews(SearchNewsDto.Post post){
        var uri = UriComponentsBuilder.fromUriString(naverNewSearchUrl) //뉴스검색 주소
                .queryParams(post.toMultiValueMap())
                .build()
                .encode()
                .toUri();

        var headers = new HttpHeaders(); //http헤더에 키값을 넣어줌
        headers.set("X-Naver-Client-Id", naverClientId);
        headers.set("X-Naver-Client-Secret", naverClientSecret);
        headers.setContentType(MediaType.APPLICATION_JSON); //Json형식으로 변환

        var httpEntity = new HttpEntity<>(headers); //요청하는 부분
        var responseType = new ParameterizedTypeReference<SearchNewsDto.Response>(){}; //응답값

        var responseEntity = new RestTemplate().exchange( //모든 정보를 담아주는 부분
                uri,
                HttpMethod.GET,
                httpEntity,
                responseType
        );
        return responseEntity.getBody();
    }

    public SearchNewsDto.Response searchQuerySet(String query, int display, String sort){
        SearchNewsDto.Post post = new SearchNewsDto.Post();
        post.setQuery(query);
        post.setStart(start);
        post.setDisplay(display);
        post.setSort(sort);

        return searchNews(post);
    }
}

 

결과화면

yml설정에서 AWS나 DB와 관련된 키값을 암호화 하려고 한다.

 

JASYPT 는 암호화에 관해 잘 모르는 개발자도 손쉽게 암호화를 할 수 있도록 돕는다.

 

 

1.  우선 gradle설정을 추가한다.

implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.3' // yml Secret Key 암호화

 

 

2.  암호화 관련 설정 클래스파일 만들기

https://github.com/ulisesbocchio/jasypt-spring-boot#use-you-own-custom-encryptor

에서 확인할 수 있다.

 

GitHub - ulisesbocchio/jasypt-spring-boot: Jasypt integration for Spring boot

Jasypt integration for Spring boot. Contribute to ulisesbocchio/jasypt-spring-boot development by creating an account on GitHub.

github.com

@Configuration
public class JasyptConfig {

    @Value("${jasypt.encryptor.password}")
    private String password;

    @Bean("jasyptStringEncryptor")
    public StringEncryptor stringEncryptor() {
        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
        SimpleStringPBEConfig config = new SimpleStringPBEConfig();
        config.setPassword(password);
        config.setAlgorithm("PBEWithMD5AndDES");
        config.setKeyObtentionIterations("1000");
        config.setPoolSize("1");
        config.setProviderName("SunJCE");
        config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
        config.setIvGeneratorClassName("org.jasypt.iv.NoIvGenerator");
        config.setStringOutputType("base64");
        encryptor.setConfig(config);
        return encryptor;
    }
}

-  나는 외부에서 키값을 입력받는 방식을 사용하였다.

 

 

3. 키값 암호화 하기

https://www.devglan.com/online-tools/jasypt-online-encryption-decryption

- 암호화하려는 키를 입력한다

- Two Way 방식을 선택

- 패스워드를 임의로 입력한다(패스워드 기억해야함) / 패스워드는 복호화시 필요하다.

 

4. 암호화된 키로 yml 설정하기

cloud:
  aws:
    region:
      static: ap-northeast-2
    s3:
      credentials:
        access-key: ENC(xFO5USJEzJjHL8RB+a7IREpyQOoHp4HZhL2OFzzKIBs=)
        secret-key: ENC(PtaZU3cqX2thf4/AeCsoMF+XCJRo3WR8q3U1iGexhAqLXaFIQ5JKn8CKkO3uRyKipF9cb/Qy4W8=)
    stack:
      auto: false
     
      
#jasypt: #패스워드 암호화/복호화 키
#  encryptor:
#    password: 설정한 패스워드

나는 AWS의 S3키를 암호화 하였다.

 

암호화된 키를 넣어줄때는  ENC(암호화된 키) 형식으로 넣어줘야 한다.

 

맨 아랫줄 패스워드에 설정한 패스워드를 넣어줘도 동작하지만 보안에 취약함으로 따로 설정하여 관리한다.

 

 

5. 패스워드 관리하기

- 직접 실행할 때 java -jar 명령어 뒤에 붙여주는 방법

--jasypt.encryptor.password=패스워드

 

- intellij 환경변수를 설정하는 방법

 

실행버튼 옆 점세개 클릭 -> 편집 -> 옵션수정 -> 환경변수 클릭 -> 환경변수 입력 후 적용 및 확인

 

 

 

- Github Actions 환경변수로 관리하는 방법

https://kjsu1994.tistory.com/42

 

java.sql.SQLIntegrityConstraintViolationException: Column 'user_id' cannot be null 오류를 해결

 

원인은 필수적으로 입력되어야 하는 컬럼에 null값이 들어갔기 때문이다

 

 

아래 코드에서 optional = false이기 null일 수 없어서 에러가 발생하였다.

@Setter @ManyToOne(optional = false) @JoinColumn(name = "userId")
private UserAccount userAccount; //유저 정보(id) 매핑

 

정확하게 매핑이 될 때까지는 optional=true를 사용해 null값을 허용하도록 하자

@Setter @ManyToOne(optional = true) @JoinColumn(name = "userId") //매핑이 완료되면 필수옵션(false)으로 바꿔줘야함
private UserAccount userAccount; //유저 정보(id) 매핑

 

이미지 파일업로드를 기존 게시글과 함께 포스트 하기위해 @ModelAttribute 애너테이션을 사용하려는데 NullPointException : null이 떠서 해결과정을 공유한다.

 

@ModelAttribute 는 @RequestBody 와 다르게 form-data형식으로 전달하기 때문에 Json타입으로 Dto에 넘어가지 않고 역직렬화 과정을 통해 매핑되어 전달이 된다. 

 

결론적으로 매핑을 하기 위해서는 결론 Dto클래스에 @NoargsContructor(혹은 생성자를 만들기) / @Setter가 필요하다 

 

나는 다음과 같이 설정해 사용했다.

 

* Controller

@PostMapping
public ResponseEntity<?> upload(@ModelAttribute MultipartFile[] files,
                                @ModelAttribute BoardDto.Post boardPostDto) throws Exception {
    Board board = boardMapper.boardPostToBoard(boardPostDto);
    board.setMember(memberService.findVerifiedMember(boardPostDto.getMemberId()));
    Board boardCreate = boardService.createBoard(board);

    List<UploadFile> uploadFiles = s3Service.uploadFiles(files, boardCreate); // aws s3업로드

    BoardDto.TotalPageResponse response = boardMapper.boardToBoardTotalPageResponse(boardCreate, uploadFiles);

    return new ResponseEntity<>(new SingleResponseDto<>(response), HttpStatus.CREATED);
}

 

@Dto

@Getter @Setter //@ModelAttribute 사용하기위함
@NoArgsConstructor //@ModelAttribute 사용하기위함
@ToString
public static class Post {
    @Positive
    @NotNull
    private Long memberId;
    @NotBlank(message = "제목을 작성해주세요")
    @Size(max = 100, message = "100자 이내로 작성해 주세요.")
    private String title;
    @NotBlank(message = "내용은 공백이 아니어야 합니다.")
    private String contents;

    public Post(Long memberId, String title, String contents) {
        this.memberId = memberId;
        this.title = title;
        this.contents = contents;
    }
}

 

 

Postman을 통해 다음과 같이 테스트하면 결과가 잘 전달되는것을 확인할 수 있었다.

 

참고 https://blog.karsei.pe.kr/59

스프링부트를 사용하여 AWS S3 버킷 연동하여 파일 업로드 하기

 

IAM사용자 키 엑세스키 생성에 이어서 스프링 부트에서 S3에 파일을 업로드할 수 있도록 구현하였다.

또한 업로드된 파일의 이름과 주소를 DB에 저장한다.

 

1. 먼저 dependencies에 의존성을 추가한다

implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

 

 

2. application.yml 설정

spring:
  jackson:
    property-naming-strategy: SNAKE_CASE
  datasource:
    url: jdbc:mysql://localhost:3306/community?serverTimezone=Asia/Seoul
    username: root
    password: password
  redis:
    host: localhost
    port: 6379
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  data: # spring data rest - 관련 설정
    rest:
      base-path: /api  #endpoint start path
      detection-strategy: annotated
  profiles:
    include: API-KEY   #key가 저장된 yml 연동


cloud: # AWS 계정연동 관련설정
  aws:
    region:
      static: ap-northeast-2
#    s3:
#      credentials:
#        access-key: access-key
#        secret-key: secret-key
    stack:
      auto: false

핵심은 spring.profies: 부터의 설정들이다

 

- spring.profiles.include : 깃에 올리지 않을 엑세스키와 시크릿키가 담긴 application-API-KEY.yml파일을 연결했다. 

환경변수를 통해 키를 관리해준다면 주석처리된 부분을 해제하여 환경변수를 통해 엑세스키와 시크릿키를 할당하면 된다.

 

- cloud설정에서 aws관련 내용들을 적어준다

 

- 마지막줄 stack.auto: false는 관련 내용을 사용하지 않음으로 반드시 적어준다.

 

 

3.  .gitignore 설정 (옵션)

### api key 관련 ###
/src/main/resources/application-API-KEY.yml

이렇게 추가해주면 키가담긴 yml파일이 깃에 올라가지 않는다.

 

 

4. 터미널에 git -rm -r -cached /src/main/resources/application-API-KEY.yml 입력

- 해당 명령어를 입력하면 기존 추적되던 캐시를 삭제한다.

 

 

5. Entity

@NoArgsConstructor
@Setter @Getter
@Entity
public class UploadFile{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long fileId;
    private String fileName;
    private String imagePath;
    @ManyToOne
    @JoinColumn(name = "board_id")
    private Board board;

    public UploadFile(String fileName, String imagePath) {
        this.fileName = fileName;
        this.imagePath = imagePath;
    }
}

- 추후 Board엔티티와 매핑을 위해 조인컬럼을 사용하였다.

 

 

6. service

@RequiredArgsConstructor
@Service
public class S3Service {

    private String S3Bucket = "kjs-project-upload"; // Bucket 이름
    private final AmazonS3Client amazonS3Client;
    private final UploadFileRepository uploadFileRepository;

    public List<String> uploadFiles(MultipartFile[] multipartFileList) throws Exception {
        List<String> imagePathList = new ArrayList<>();

        for (MultipartFile multipartFile : multipartFileList) {
            String fileName = multipartFile.getOriginalFilename(); // 파일 이름
            long size = multipartFile.getSize(); // 파일 크기

            ObjectMetadata objectMetaData = new ObjectMetadata();
            objectMetaData.setContentType(multipartFile.getContentType());
            objectMetaData.setContentLength(size);

            // S3에 업로드
            amazonS3Client.putObject(
                    new PutObjectRequest(S3Bucket, fileName, multipartFile.getInputStream(), objectMetaData)
                            .withCannedAcl(CannedAccessControlList.PublicRead)
            );

            String imagePath = amazonS3Client.getUrl(S3Bucket, fileName).toString(); // 접근가능한 URL 가져오기
            imagePathList.add(imagePath); //String Type URL주소

            //엔티티에 저장하는 로직

            UploadFile uploadFile = new UploadFile();
            uploadFile.setFileName(fileName);
            uploadFile.setImagePath(imagePath);
            uploadFileRepository.save(uploadFile);
        }
        return imagePathList;
    }
}

 

 

 

7. Repository

public interface UploadFileRepository extends JpaRepository<UploadFile, Long> {}

- @Repository 는 상속받는 JPARpository에 포함되어있기 때문에 적지 않아도 된다.

 

 

8. Contorller

@RestController
@RequiredArgsConstructor
public class BoardController {
    private final S3Service s3Service;

    @PostMapping("/upload")
    public ResponseEntity<Object> upload(@RequestParam MultipartFile[] files) throws Exception {
        List<String> imagePathList = s3Service.uploadFiles(files);

        return new ResponseEntity<>(imagePathList, HttpStatus.OK);
    }

 

 

9. Postman 테스트

- body에 기존 Json형식으로 데이터를 넣는것이 아닌 form-data형식으로 데이터를 넣어줘야한다.

 

 

결과값으로 사진파일을 확인할 수 있는 S3주소를 리턴한다.

 

또한 연결된 DB를 확인해보면 해당 파일명과 S3버킷 안에있는 파일URL이 저장된 것을 확인할 수 있다.

 

 

참고 : https://jforj.tistory.com/261

두가지 방법을 통한 설정으로 간단하게 변환할 수 있다.

 

1. 개별적으로 @JsonProperty(" 출력값 ") 을 할당해주면 된다

@Setter @Getter
@AllArgsConstructor
@NoArgsConstructor
public static class Response {
    @JsonProperty("comment_id")
    private Long commentId;
    private String contents;
    @JsonProperty("like_count")
    private int likeCount;
    @JsonProperty("create_at")
    private LocalDateTime createdAt;
    @JsonProperty("modified_at")
    private LocalDateTime modifiedAt;
    @JsonProperty("board_id")
    private long boardId;
    @JsonProperty("member_id")
    private long memberId;
}

2. 

application.yml 설정으로 변환하기

spring:
  jackson:
    property-naming-strategy: SNAKE_CASE

 

 

 

검색기능 사용을 위한 기본편 이후 controller부에서 사용하기 위한 service로직 구현까지의 내용이다

 

 

@RepositoryRestResource // spring data rest 사용하기 위함
public interface ArticleRepository extends
        JpaRepository<Article, Long>,
        QuerydslPredicateExecutor<Article>, //해당 엔티티 안에있는 모든 검색기능을 추가해줌 // 완전 동일해야만 동작
        QuerydslBinderCustomizer<QArticle> // 부분검색, 대소문자 구분 등을 위함
{
    Page<Article> findByTitleContaining(String title, Pageable pageable); //제목검색
    Page<Article> findByContentContaining(String content, Pageable pageable); //내용검색/ Containing-부분검색
//    Page<Article> findByUserAccount_UserIdContaining(String userId, Pageable pageable); //회원id검색
//    Page<Article> findByUserAccount_NicknameContaining(String nickName, Pageable pageable); //닉네임검색
    Page<Article> findByHashtag(String hashtag, Pageable pageable); //제목검색

    @Override
    default void customize(QuerydslBindings bindings, QArticle root){
        //Article에 대해 선택적인 필드에 대한 검색
        bindings.excludeUnlistedProperties(true); //리스팅 하지 않은 프로퍼티 검색 제외
        bindings.including(root.title, root.content, root.hashtag, root.createdAt, root.createdBy); //검색 컬럼
        bindings.bind(root.title).first(StringExpression::containsIgnoreCase); // like '%${v}%'/ 부분검색
        bindings.bind(root.content).first(StringExpression::containsIgnoreCase);
        bindings.bind(root.hashtag).first(StringExpression::containsIgnoreCase);
        bindings.bind(root.createdAt).first(DateTimeExpression::eq);//원하는 날짜검색 /시분초 동일하게 넣어야함
        bindings.bind(root.createdBy).first(StringExpression::containsIgnoreCase);
    }
}

* Repository

레파지토리 부분에 서비스 로직에 사용할 메서드를 추가하였다.

Containing은 부분검색을 가능하게 만들어준다.

 

@Slf4j
@RequiredArgsConstructor
@Transactional //import annotation 유의해서 가져오기
@Service
public class ArticleService {
    private final ArticleRepository articleRepository;
    //게시글 페이지형 검색
    @Transactional(readOnly = true) // 변경하지 않기때문에 readonly
    public Page<Article> searchArticles(SearchType searchType, String search_keyword, Pageable pageable) {
        // 검색어 없이 검색하면 게시글 페이지를 반환.
        if (search_keyword == null || search_keyword.isBlank()) {
            return articleRepository.findAll(pageable);
        }
        // 항목에 따른 검색 - 조회
        switch (searchType) { // return이 되는 switch ->와 함께 사용 java14이상부터 사용가능
            case TITLE:
                return articleRepository.findByTitleContaining(search_keyword, pageable);
            case CONTENT:
                return articleRepository.findByContentContaining(search_keyword, pageable);
//            case ARTICLE_ID:
//                return articleRepository.findByUserAccount_UserIdContaining(search_keyword, pageable);
//            case NICKNAME:
//                return articleRepository.findByUserAccount_NicknameContaining(search_keyword, pageable);
            case HASHTAG:
                return articleRepository.findByHashtag(search_keyword, pageable);
        }
        return null;
    }

* Service

일치하는 검색어가 없으면 전체 게시글 페이지를 반환하고 있으면 해당하는 페이지만 반환하도록 로직을 구성하였다.

 

 

public enum SearchType {
    TITLE, CONTENT, ARTICLE_ID, NICKNAME, HASHTAG
}

 

엔티티 검색을 하기위해 필요한 컬럼명 등록

 

 

@RequiredArgsConstructor
@Validated
@RequestMapping("/articles")
@RestController
public class ArticleController {

    private final ArticleService articleService;
    private final ArticleMapper articleMapper;



    @GetMapping //부분검색 //http://localhost:8080/articles?searchType=CONTENT&searchValue=검색어
    public ResponseEntity searchArticles(@RequestParam(required = false)SearchType searchType,//required = false- 선택적 파라미터
                           @RequestParam(required = false)String searchValue,
                           @PageableDefault(size = 10,sort = "createdAt", direction = Sort.Direction.DESC)Pageable pageable)
    {
        Page<Article> articlePages = articleService.searchArticles(searchType, searchValue, pageable);
        List<Article> articles = articlePages.getContent();
        List<ArticleDto.Response> response = articleMapper.articleToArticleListResponse(articles);
        return new ResponseEntity(response, HttpStatus.OK);
    }

* Controller

부분검색기능을 받는 컨트롤러에 Getmapping부

@PageableDefault를 통해 pageble을 입력해주지 않으면 디폴트값이 들어가게 된다.

 

content내용 검색을 위한 파라미터 입력 예) searchType=CONTENT&searchValue=검색내용

 

 

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ArticleMapper {
    Article articlePostToArticle(ArticleDto.Post articlePostDto);
    Article articlePatchToArticle(ArticleDto.Patch articlePatchDto);
    ArticleDto.Response articleToArticleResponse(Article article);
    List<ArticleDto.Response> articleToArticleListResponse(List<Article> articles);
}

* Mapper

컨트롤러 부에서 엔티티를 ResponseDto로 변환할 매퍼 검색기능에서는 페이지를 반환하기 위해

List<response>가  사용된다.

 

 

@Getter
@ToString(callSuper = true) //callSuper = true - 안쪽까지 들어가서 ToString을 찍어냄
@Table(indexes = {
        @Index(columnList = "title"),
        @Index(columnList = "hashtag"),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy"),
})
@Entity
@NoArgsConstructor//(access = AccessLevel.PROTECTED) //기본생성자의 무분별한 생성을 막아서 의도치않는 엔티티 생성을 막음
public class Article extends AuditingFields {
    /* @Setter을 클래스레벨로 설정하지 않는 이유 id, 시간 등 을 Setter 하지 않기 위함*/
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long articleId;
   // @Setter @ManyToOne(optional = false)
    //private UserAccount userAccount; //유저 정보(id) 매핑
    @Setter @Column(nullable = false)
    private String title; // 제목
    @Setter @Column(nullable = false, length = 10000)
    private String content; // 본문
    @Setter
    private String hashtag; // 해시태그

    @OrderBy("createdAt DESC") // 생성 시간순 정렬
    @OneToMany(mappedBy = "article", cascade = CascadeType.ALL)// 모든경우 외래키를 걸어줌/ 실무에선 양방향바인딩 지양
    @ToString.Exclude //조회하는 과정에서 무한 순환참조 방지, 퍼포먼스 이슈로 인하여 설정 / 연결고리를 끊어줌
    private final Set<ArticleComment> articleComments = new LinkedHashSet<>();

    public Article(String title, String content, String hashtag) { //도메인과 관련있는 정보만 오픈
        //this.userAccount = userAccount;
        this.title = title;
        this.content = content;
        this.hashtag = hashtag;
    }
/**
    //new를 사용하지 않고 생성자 생성하기 위함  // of는 매개변수가 뭐가필요한지 가이드해줌
    public static Article of(String title, String content, String hashtag) {
        return new Article(title, content, hashtag);
    } // @Mapper 사용 할때에는 쓸수없는 방법
**/

    //equalse 해시코드 기능 생성(alt+insert에 있음) / id만 같으면 같은게시물
    @Override //동일성 검사
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Article)) return false;
        Article article = (Article) o;
        return articleId!=null && articleId.equals(article.articleId); //데이터가 insert되기 전에 만들어진 id는 탈락
    }

    @Override
    public int hashCode() { //동일성 검사를 마친 아이디에 대해서만 해시
        return Objects.hash(articleId);
    }
}

* Entity

검색 대상 엔티티

 

 

postman URI  ex) http://localhost:8080/articles?searchType=CONTENT&searchValue=검색

- article엔티티내의 content컬럼에서 검색이라는 단어가 포함되어있는 항목들을 모두 반환한다.

Security 설정후 잘 작동하던 PostMapping 컨트롤러가 작동하지 않았다.

 

CSRF() 로인해서 post 요청이 막히는것.

        http.csrf().disable();

SecurityConfig.java 의 SecurityFilterChain을 상속받은 메서드에 위 코드를 추가해주면 해결된다.

 

@Configuration // 설정값으로 사용
public class SecurityConfig {

    @Bean // SecurityAdapter상속 대신 사용 /최신  // 시큐리티 기본 로그인화면 탈출하기위함
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf().disable() // CSRF() 로인해서 post 요청이 막히는것을 해제해줌 / post 403에러 해결
                .authorizeHttpRequests(auth -> auth //auth를 받아서 어떤 리퀘스트를 열것인지
                        .antMatchers(HttpMethod.POST, "/articles").permitAll() //Role메서드 추가예정
                        .anyRequest().permitAll()) //auth를 받아서 어떤리퀘스트든 다열겠다
                .formLogin().and() //폼로그인 사용
                .build();
    }

Security설정관련 메서드 초반부 작성내용

 

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass

핵심은 위의 두가지 애너테이션이다

package main22.community.domain;

import lombok.Getter;
import lombok.ToString;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.format.annotation.DateTimeFormat;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
@ToString
@EntityListeners(AuditingEntityListener.class)//엔티티 Auditing이 동작하기위해 필수추가
@MappedSuperclass  //Jpa애너테이션
public class AuditingFields { // 공통 필드에 대한 처리

    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) //파라미터 파싱에 대한 룰
    @CreatedDate
    @Column(nullable = false, updatable = false) //수정불가
    private LocalDateTime createdAt; // 생성일시
    @CreatedBy
    @Column(nullable = false, length = 100)//누가 만들었는지에 대한 정보 JpaConfig에서 확인
    private String createdBy; // 생성자
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) //파라미터 파싱에 대한 룰
    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime modifiedAt; // 수정일시
    @LastModifiedBy
    @Column(nullable = false, length = 100)
    private String modifiedBy; // 수정자
}

이렇게 공통기능으로 자주쓰는 생성, 수정시간 관련된 컬럼을 따로 클래스로 따로 관리하고 엔티티 클래스에 상속해주면 코드를 줄일 수 있다.

 

또한 클래스로 따로 관리하면 공통기능에 대한 수정도 편리하다

 

 

 

rest api요청을 보낼때 해당 엔티티에 대한 검색기능을 사용할 수 있는 기술이다.

 

- 먼저 build.gradle 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'com.mysql:mysql-connector-j'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-data-rest' // data rest기능 / api빠르게만들수있음
    implementation 'org.springframework.data:spring-data-rest-hal-explorer' // 해당내용을 시각적으로 보는것
    runtimeOnly 'com.h2database:h2'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // queryDSL 설정
    // 엔티티 검색기능 사용 가능
    implementation "com.querydsl:querydsl-jpa"
    implementation "com.querydsl:querydsl-core"
    implementation "com.querydsl:querydsl-collections"
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // querydsl JPAAnnotationProcessor 사용 지정
    annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError (javax.annotation.Generated) 대응 코드
    annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError (javax.annotation.Entity) 대응 코드
}

tasks.named('test') {
    useJUnitPlatform()
}

// Querydsl 설정부
def generated = 'src/main/generated'  //파일경로  
// build디렉터리 안에 있는걸 눈에 보이게 꺼내옴
// ide같은 툴을 사용해서 발생할 수 있는 잠재적 문제를 해결하기위함

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// java source set 에 querydsl QClass 위치 추가
sourceSets {
    main.java.srcDirs += [ generated ]
}

// gradle clean 시에 QClass 디렉토리 삭제
clean {
    delete file(generated)  //build clean할때 해당 파일도 같이 삭제

- Article 엔티티에 대한 레파지토리

package main22.community.repository;

import com.querydsl.core.types.dsl.DateTimeExpression;
import com.querydsl.core.types.dsl.StringExpression;
import main22.community.domain.entity.Article;
import main22.community.domain.entity.QArticle;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer;
import org.springframework.data.querydsl.binding.QuerydslBindings;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
//@Repository안붙여도 정상동작함 상속하는 클래스에 이미 붙어있음
@RepositoryRestResource // spring data rest 사용하기 위함
public interface ArticleRepository extends
        JpaRepository<Article, Long>,
        QuerydslPredicateExecutor<Article>, //해당 엔티티 안에있는 모든 검색기능을 추가해줌 // 완전 동일해야만 동작
        QuerydslBinderCustomizer<QArticle> // 부분검색, 대소문자 구분 등을 위함
{
    @Override
    default void customize(QuerydslBindings bindings, QArticle root){
        //Article에 대해 선택적인 필드에 대한 검색
        bindings.excludeUnlistedProperties(true); //리스팅 하지 않은 프로퍼티 검색 제외
        bindings.including(root.title, root.content, root.hashtag, root.createdAt, root.createdBy); //검색 컬럼
        bindings.bind(root.title).first(StringExpression::containsIgnoreCase); // like '%${v}%'/ 부분검색
        bindings.bind(root.content).first(StringExpression::containsIgnoreCase);
        bindings.bind(root.hashtag).first(StringExpression::containsIgnoreCase);
        bindings.bind(root.createdAt).first(DateTimeExpression::eq);//원하는 날짜검색 /시분초 동일하게 넣어야함
        bindings.bind(root.createdBy).first(StringExpression::containsIgnoreCase);
    }
}

- application.yml 설정부

spring:
  data: # spring data rest - 관련 설정
    rest:
      base-path: /api  #endpoint start path
      detection-strategy: annotated

 

article엔티티의 title 테이블 검색방법 ex) http://localhost:8080/api/articles?title="(부분)검색" 으로 사용하면 된다

+ Recent posts