Skip to content

πŸ‘» κ²Œμ‹œνŒ (Spring Boot + React)

Notifications You must be signed in to change notification settings

hellonayeon/bbs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Bulletin Board System

κ²Œμ‹œνŒ μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜ ν”„λ‘œμ νŠΈ μž…λ‹ˆλ‹€.

2022.08.04 ~ 2022.08.07 λ™μ•ˆ Spring Boot 와 React λ₯Ό μ‚¬μš©ν•΄ κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€.

이 ν”„λ‘œμ νŠΈλ₯Ό 톡해 이루고자 ν•œ λͺ©ν‘œλŠ” Springμ—μ„œ μ œκ³΅ν•˜λŠ” ν”„λ ˆμž„μ›Œν¬λ₯Ό 직접 μ‚¬μš©ν•΄ 보기 μ˜€μŠ΅λ‹ˆλ‹€. ν”„λ‘œμ νŠΈ κ΅¬ν˜„ κ³Όμ • λ™μ•ˆ νšŒμ› 인증/인가, μ˜ˆμ™Έμ²˜λ¦¬λ₯Ό κ³ λ―Όν•˜λ©° μ½”λ“œλ₯Ό μž‘μ„±ν–ˆμŠ΅λ‹ˆλ‹€.

πŸ“š λͺ©μ°¨

πŸŽƒ ν”„λ‘œμ νŠΈ ꡬ쑰

πŸ“Œ Backend

backend-project-structure

πŸ₯• Frontend

frontend-project-structure

πŸ•Ή μ‚¬μš© 기술

πŸ“Œ Backend

기술 버전
Spring Boot 2.7.2
Spring Security 2.7.2
Bean Validation 2.7.2
JSON Web Token 0.9.1
MyBatis 2.1.3
MySQL Connector J 8.0.28
Swagger 3.0.0

πŸ₯• Frontend

기술 버전
NodeJS 16.16.0
React 18.2.0
axios 0.27.2
react-axios 2.0.6
react-dom 18.2.0
react-js-pagination 3.0.3
react-router 6.3.0
react-router-dom 6.3.0
react-scripts 5.0.1

🎒 κ΅¬ν˜„ κΈ°λŠ₯

  • κ²Œμ‹œνŒ κΈ°λŠ₯
    • λͺ¨λ“  κ²Œμ‹œκΈ€ 및 νŠΉμ • κ²Œμ‹œκΈ€ 쑰회
    • κ²Œμ‹œκΈ€ 검색 (제λͺ©, λ‚΄μš©, μž‘μ„±μž)
    • κ²Œμ‹œκΈ€ μž‘μ„± [νšŒμ›]
    • κ²Œμ‹œκΈ€ μˆ˜μ • [νšŒμ›, κ²Œμ‹œκΈ€ μž‘μ„±μž]
    • κ²Œμ‹œκΈ€ μ‚­μ œ [νšŒμ›, κ²Œμ‹œκΈ€ μž‘μ„±μž]
    • κ²Œμ‹œκΈ€ λ‹΅κΈ€ μž‘μ„± [νšŒμ›]
  • λŒ“κΈ€ κΈ°λŠ₯
    • λŒ“κΈ€ 쑰회
    • λŒ“κΈ€ μž‘μ„± [νšŒμ›]
    • λŒ“κΈ€ μˆ˜μ • [νšŒμ›, λŒ“κΈ€ μž‘μ„±μž]
    • λŒ“κΈ€ μ‚­μ œ [νšŒμ›, λŒ“κΈ€ μž‘μ„±μž]
  • νšŒμ› κΈ°λŠ₯
    • νšŒμ›κ°€μž…
    • 둜그인/λ‘œκ·Έμ•„μ›ƒ

🍭 κΈ°λŠ₯ μ‹€ν–‰ν™”λ©΄

κ²Œμ‹œνŒ κΈ°λŠ₯

λͺ¨λ“  κ²Œμ‹œκΈ€ 및 νŠΉμ • κ²Œμ‹œκΈ€ 쑰회

  • λͺ¨λ“  κ²Œμ‹œκΈ€μ„ μ‘°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€. νŽ˜μ΄μ§• κΈ°λŠ₯을 톡해 ν•œ νŽ˜μ΄μ§€μ—μ„œ μ΅œλŒ€ 10개의 κ²Œμ‹œκΈ€μ΄ μ‘°νšŒλ©λ‹ˆλ‹€.

bbslist1

bbslist2

  • κ²Œμ‹œκΈ€μ˜ 제λͺ©μ„ ν΄λ¦­ν•˜λ©΄, κ²Œμ‹œκΈ€μ˜ 상세 λ‚΄μš©μ„ μ‘°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

bbsdetail1

bbsdetail2

κ²Œμ‹œκΈ€ 검색

  • κ²Œμ‹œκΈ€μ˜ 제λͺ©κ³Ό λ‚΄μš© λ˜λŠ” μž‘μ„±μžλ‘œ κ²Œμ‹œκΈ€μ„ 검색할 수 μžˆμŠ΅λ‹ˆλ‹€.

bbs-search

κ²Œμ‹œκΈ€ μž‘μ„±

  • λ‘œκ·ΈμΈν•œ μ‚¬μš©μžλŠ” κ²Œμ‹œκΈ€μ„ μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

bbs-write

bbs-write-success

bbs-write-result

  • λ‘œκ·ΈμΈν•˜μ§€ μ•Šμ•˜μ„ 경우 κΈ€ μž‘μ„±μ΄ μ œν•œλ©λ‹ˆλ‹€.

bbs-write-auth

κ²Œμ‹œκΈ€ μˆ˜μ •

  • κ²Œμ‹œκΈ€ μž‘μ„±μžλŠ” κ²Œμ‹œκΈ€μ„ μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

bbs-update

bbs-update2

bbs-update-success

  • μžμ‹ μ΄ μž‘μ„±ν•œ κ²Œμ‹œκΈ€μ—λ§Œ μˆ˜μ •, μ‚­μ œ λ²„νŠΌμ΄ ν™œμ„±ν™”λ©λ‹ˆλ‹€. bbs-update-delete-btn-deactive

bbs-update-delete-btn-active

κ²Œμ‹œκΈ€ μ‚­μ œ

  • κ²Œμ‹œκΈ€ μž‘μ„±μžλŠ” κ²Œμ‹œκΈ€μ„ μ‚­μ œν•  수 μžˆμŠ΅λ‹ˆλ‹€.

bbs-delete

bbs-delete-result

κ²Œμ‹œκΈ€ λ‹΅κΈ€ μž‘μ„±

  • ν•˜λ‚˜μ˜ κ²Œμ‹œκΈ€μ— λŒ€ν•œ 닡글을 μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€. κ²Œμ‹œκΈ€ μž‘μ„± κ³Ό λ§ˆμ°¬κ°€μ§€λ‘œ λ‘œκ·ΈμΈν•œ μ‚¬μš©μžλ§Œ 닡글을 μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

bbs-answer1

bbs-answer2

bbs-answer3

bbs-answer4

λŒ“κΈ€ κΈ°λŠ₯

λŒ“κΈ€ 쑰회

  • κ²Œμ‹œκΈ€ 상세 μ—μ„œ κ΄€λ ¨λœ λŒ“κΈ€μ„ μ‘°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€. νŽ˜μ΄μ§• κΈ°λŠ₯을 톡해 ν•œ νŽ˜μ΄μ§€μ—μ„œ μ΅œλŒ€ 5개의 λŒ“κΈ€μ΄ μ‘°νšŒλ©λ‹ˆλ‹€.

comment1

comment2

λŒ“κΈ€ μž‘μ„±

  • λ‘œκ·ΈμΈν•œ μ‚¬μš©μžλŠ” λŒ“κΈ€μ„ μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

comment-write1

comment-write2

λŒ“κΈ€ μˆ˜μ •

  • μžμ‹ μ΄ μž‘μ„±ν•œ λŒ“κΈ€μ„ μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

comment-update1

comment-update2

comment-update3

λŒ“κΈ€ μ‚­μ œ

  • μžμ‹ μ΄ μž‘μ„±ν•œ λŒ“κΈ€μ„ μ‚­μ œν•  수 μžˆμŠ΅λ‹ˆλ‹€.

comment-delete

comment-delete2

νšŒμ› κΈ°λŠ₯

νšŒμ›κ°€μž…

  • νšŒμ›κ°€μž… μ‹œ 아이디 쀑볡을 μ²΄ν¬ν•©λ‹ˆλ‹€.

signup-idcheck

  • νšŒμ›κ°€μž…μ„ 톡해 μ„œλΉ„μŠ€μ— μ‚¬μš©μž 정보λ₯Ό μ €μž₯ν•©λ‹ˆλ‹€.

signup-success

둜그인/λ‘œκ·Έμ•„μ›ƒ

  • 둜그인

login-form

login-success

  • λ‘œκ·ΈμΈμ„ μ™„λ£Œν•˜λ©΄ λΈŒλΌμš°μ €μ˜ Local Storage 에 μ‚¬μš©μž id 와 JWT 토큰 정보λ₯Ό μ €μž₯ν•©λ‹ˆλ‹€.

login-after-devtool

  • λ‘œκ·Έμ•„μ›ƒ

logout

  • λ‘œκ·Έμ•„μ›ƒμ„ μ™„λ£Œν•˜λ©΄ λΈŒλΌμš°μ €μ˜ Local Storage 의 λ‚΄μš©λ„ μ‚­μ œν•©λ‹ˆλ‹€.

logout-after-devtool

πŸ€™πŸ» API λͺ…μ„Έμ„œ

HTTP λ©”μ„œλ“œλ₯Ό 톡해 ν–‰μœ„λ₯Ό λͺ…μ‹œν•  수 μžˆλ„λ‘ RESTful λ°©μ‹μœΌλ‘œ μ„€κ³„ν–ˆμŠ΅λ‹ˆλ‹€.

api-definition

πŸ•Έ ERD 섀계

erd

πŸ‘Ύ νŠΈλŸ¬λΈ”μŠˆνŒ…

νšŒμ› 인증 및 인가 κΈ°λŠ₯ κ΅¬ν˜„ (Spring Security + JWT)

νšŒμ› 및 λΉ„νšŒμ›μ— 따라 κ°€μš©ν•œ κΈ°λŠ₯에 μ œμ•½μ„ 두기 μœ„ν•΄ Spring Security + JWT 토큰 λ°©μ‹μœΌλ‘œ κ΅¬ν˜„ν–ˆλ‹€.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/bbs", "/comment").authenticated()
.antMatchers(HttpMethod.PATCH, "/bbs", "/comment").authenticated()
.antMatchers(HttpMethod.DELETE, "/bbs", "/comment").authenticated()
.anyRequest().permitAll();
http.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}

@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
String userId = null;
String authToken = null;
if (header != null && header.startsWith(TOKEN_PREFIX)) {
authToken = header.replace(TOKEN_PREFIX,"");
try {
userId = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (IllegalArgumentException e) {
System.out.println("JwtAuthenticationFilter: token error (fail get user id) !");
e.printStackTrace();
} catch (ExpiredJwtException e) {
System.out.println("JwtAuthenticationFilter: expired token !");
e.printStackTrace();
} catch(SignatureException e){
System.out.println("JwtAuthenticationFilter: invalid member !");
e.printStackTrace();
}
} else {
System.out.println("JwtAuthenticationFilter: request that do not require authorization.");
}
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("authenticated user " + userId + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(req, res);
}

κ°„λ‹¨ν•œ BBS μ‹œμŠ€ν…œμ„ λ§Œλ“œλŠ” 것을 λͺ©ν‘œλ‘œ ν–ˆμœΌλ‚˜, Spring Security λ₯Ό μ‚¬μš©ν•˜λ‹ˆ 인증 λ‘œμ§μ„ κ΅¬ν˜„ν•˜κΈ° λ³΅μž‘ν–ˆλ‹€. Spring Security 의 λ™μž‘ 원리λ₯Ό μ •ν™•ν•˜κ²Œ λͺ¨λ₯΄λŠ” μƒνƒœμ—μ„œ κ΅¬ν˜„ν•˜λ € ν•˜λ‹ˆ 어렀움이 μžˆμ—ˆλ‹€. (배보닀 배꼽이 더 컀진 λŠλ‚Œ 🫀)

Spring μ—μ„œ μ œκ³΅ν•˜λŠ” Interceptor κΈ°λŠ₯κ³Ό ArgumentResolver, Annotation κΈ°λŠ₯을 μ‚¬μš©ν•˜κ³  μ‚¬μš©μž μ•„μ΄λ””λ§Œμ„ λ°›μ•„ 인가 체크λ₯Ό ν–ˆλ‹€λ©΄ κ°„λ‹¨ν•˜κ²Œ κ΅¬ν˜„ κ°€λŠ₯ν–ˆμ„ 것 κ°™λ‹€.

μš”μ²­μ„ 보낸 μ‚¬μš©μžλ₯Ό νŒλ³„ν•˜κΈ° μœ„ν•΄ @AuthenticationPrincipal 을 μ‚¬μš©ν•˜μ—¬, 둜그인 μ‹œ μΈμ¦ν•œ ν›„ μ €μž₯ν•œ μ‚¬μš©μž 정보인 UserDetails 의 username(Id) λ₯Ό 가져와 κΈ€ μž‘μ„±μžμ™€ λΉ„κ΅ν–ˆλ‹€.

/* [PATCH] /comment/{seq} λŒ“κΈ€ μˆ˜μ • */
@PatchMapping("/{seq}")
public ResponseEntity<UpdateCommentResponse> updateComment(@AuthenticationPrincipal UserDetails userDetails,
@PathVariable Integer seq,
@RequestBody UpdateCommentRequest req) {
return ResponseEntity.ok(service.updateComment(userDetails.getUsername(), seq, req));
}

/* λŒ“κΈ€ μˆ˜μ • */
@Transactional
public UpdateCommentResponse updateComment(String id, Integer seq, UpdateCommentRequest req) {
Comment comment = dao.getCommentBySeq(seq);
if (!comment.getId().equals(id)) {
System.out.println("μž‘μ„±μžλ§Œ λŒ“κΈ€μ„ μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.");
return null;
}
Integer updatedRecordCount = dao.updateComment(new UpdateCommentParam(seq, req.getContent()));
if (updatedRecordCount != 1) {
System.out.println("λŒ“κΈ€ μˆ˜μ •μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.");
return null;
}
return new UpdateCommentResponse(updatedRecordCount);
}



μ‚¬μš©μž 둜그인 μš”μ²­ 데이터 검증과 μ‚¬μš©μž 인증 μ˜ˆμ™Έ 처리

μ‚¬μš©μžκ°€ νšŒμ›κ°€μž…κ³Ό λ‘œκ·ΈμΈμ„ μœ„ν•΄ μž…λ ₯ν•œ 데이터에 λŒ€ν•΄ Bean Validation 을 μ‚¬μš©ν•΄ 검증 κΈ°λŠ₯을 κ΅¬ν˜„ν–ˆλ‹€.

ν…œν”Œλ¦Ώ μ—”μ§„μœΌλ‘œ μ„œλ²„μ—μ„œ λ·°λ₯Ό κ·Έλ¦¬λŠ” 방식이 μ•„λ‹Œ, λ°μ΄ν„°λ§Œ μ „μ†‘ν•˜λŠ” API μ„œλ²„μ—μ„œ μž…λ ₯κ°’ 검증에 λŒ€ν•œ 였λ₯˜ λ©”μ‹œμ§€λ₯Ό μ–΄λ–»κ²Œ 전달해야 할지 κ³ λ―Όν–ˆμ—ˆλ‹€.

Bean Validation μ—μ„œ λ˜μ§„ μ˜ˆμ™Έλ₯Ό λ°›μ•„μ„œ μ²˜λ¦¬ν•œ λ‹€μŒ 였λ₯˜ λ©”μ‹œμ§€λ₯Ό μ‘λ‹΅μœΌλ‘œ 내렀주도둝 κ΅¬ν˜„ν–ˆλ‹€.

  • νšŒμ›κ°€μž… μš”μ²­ 폼 데이터

public class JoinRequest {
@NotBlank
private String id;
@NotBlank
private String name;
@NotBlank
private String pwd;
@NotBlank
private String checkPwd;
@NotBlank
private String email;

  • νšŒμ› κ°€μž… μš”μ²­ 처리 컨트둀러

/* μš”μ²­ DTO 검증 μ˜ˆμ™Έμ²˜λ¦¬ ν•Έλ“€λŸ¬ */
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
System.out.println("UserController handleMethodArgumentNotValidException " + new Date());
BindingResult bs = e.getBindingResult();
StringBuilder sb = new StringBuilder();
bs.getFieldErrors().forEach(err -> {
sb.append(String.format("[%s]: %s.\nμž…λ ₯된 κ°’: %s",
err.getField(), err.getDefaultMessage(), err.getRejectedValue()));
});
// Map 으둜 보낸닀면 ν”„λ‘ νŠΈμ—μ„œ μ‚¬μš©ν•˜κΈ° 더 νŽΈλ¦¬ν•  λ“― !
return new ResponseEntity<>(sb.toString(), HttpStatus.BAD_REQUEST);
}

λ˜ν•œ μ‚¬μš©μž κ΄€λ ¨ μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄ μ»€μŠ€ν…€ μ˜ˆμ™Έλ₯Ό λ§Œλ“€μ–΄μ„œ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚€κ³  ν•Έλ“€λŸ¬μ—μ„œ μ²˜λ¦¬ν•  수 μžˆλ„λ‘ κ΅¬ν˜„ν–ˆλ‹€.

private void isExistUserId(String id) {
Integer result = dao.isExistUserId(id);
if (result == 1) {
throw new MemberException("이미 μ‚¬μš©μ€‘μΈ μ•„μ΄λ””μž…λ‹ˆλ‹€.", HttpStatus.BAD_REQUEST);
}
}
private void checkPwd(String pwd, String checkPwd) {
if (!pwd.equals(checkPwd)) {
throw new MemberException("λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", HttpStatus.BAD_REQUEST);
}
}

/* μ‚¬μš©μž κ΄€λ ¨ μš”μ²­ μ˜ˆμ™Έμ²˜λ¦¬ ν•Έλ“€λŸ¬ */
@ExceptionHandler(MemberException.class)
public ResponseEntity<?> handleUserException(MemberException e) {
System.out.println("UserController handlerUserException " + new Date());
return new ResponseEntity<>(e.getMessage(), e.getStatus());
}

@AdviceController λ₯Ό 더 많이 μ‚¬μš©ν•œλ‹€κ³  ν–ˆλŠ”λ°, 이 ν”„λ‘œμ νŠΈμ—μ„œλŠ” νšŒμ› κ΄€λ ¨λœ μ˜ˆμ™Έ μ²˜λ¦¬λ§Œμ„ μž‘μ„±ν•˜κΈ° μœ„ν•΄ μ „μ—­ μ˜ˆμ™Έμ²˜λ¦¬λ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šμ•˜λ‹€. λ˜ν•œ ν‰μ†Œμ—λ„ μ»¨νŠΈλ‘€λŸ¬λ³„λ‘œ μ²˜λ¦¬ν•˜λŠ” μ˜ˆμ™Έλ₯Ό μ„ΈλΆ„ν™” ν•˜λŠ”κ²Œ 쒋지 μ•Šμ„κΉŒ? λΌλŠ” 생각을 가지고 μžˆμ—ˆλ‹€.

ν•˜μ§€λ§Œ κ²°κ΅­ μ–΄λ–€ μ˜ˆμ™Έμ²˜λ¦¬λ“  μ„œλ²„μ—μ„œ λ°œμƒν•œ μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•œ λ‹€μŒ μš”μ²­μ„ 보낸 ν”„λ‘ νŠΈ μͺ½μœΌλ‘œ μƒνƒœ μ½”λ“œλ“  였λ₯˜ λ©”μ‹œμ§€λ“  λ‚΄λ €μ€˜μ•Ό ν•œλ‹€. λ”°λΌμ„œ μ»€μŠ€ν…€ μ˜ˆμ™Έμ™€ μ˜ˆμ™Έ λ©”μ‹œμ§€λ₯Ό λ²”μš©μ„± 있게 μž‘μ„±ν•˜λ©΄ 였히렀 μ „μ—­μ—μ„œ μ˜ˆμ™Έμ²˜λ¦¬ ν•˜λŠ”κ²Œ 일관성 μžˆμ„ κ²ƒμ΄λΌλŠ” κΉ¨λ‹¬μŒμ„ μ–»μ—ˆλ‹€.



DTO 클래슀 뢄리

μš”μ²­κ³Ό μ‘λ‹΅μœΌλ‘œ μ£Όκ³ λ°›λŠ” 데이터λ₯Ό ν•œ λˆˆμ— ν™•μΈν•˜κΈ° μœ„ν•΄ μš”μ²­ 데이터와 응닡 데이터λ₯Ό 각각의 DTO둜 λΆ„λ¦¬ν–ˆλ‹€.

μš”μ²­μœΌλ‘œ 받은 데이터λ₯Ό λ°”νƒ•μœΌλ‘œ SQL 쿼리λ₯Ό μˆ˜ν–‰ν•  λ•Œ ν•„μš”ν•œ λ°μ΄ν„°λ§Œμ„ λ„˜κ²¨μ£ΌκΈ° μœ„ν•΄ Serviceμ—μ„œ Dao둜 λ„˜κΈ°λŠ” νŒŒλΌλ―Έν„°λ„ DTO둜 λΆ„λ¦¬ν–ˆλ‹€.

project-structure

μ΄λ ‡κ²Œ κ΅¬ν˜„ν–ˆμ„ λ•Œμ˜ μž₯점은 컨트둀러 λ©”μ„œλ“œμ˜ νŒŒλΌλ―Έν„°λ‘œ λ§Žμ€ 인자λ₯Ό λ„˜κ²¨μ£Όμ§€ μ•Šμ•„λ„ λœλ‹€λŠ” 점, μ£Όκ³ λ°›λŠ” 데이터λ₯Ό ν™•μΈν•˜κ³  μˆ˜μ •ν•΄μ•Όν•˜λŠ” 경우 DTO 클래슀 λ§Œμ„ μˆ˜μ •ν•΄μ•Ό ν•˜λŠ” μ μ΄μ—ˆλ‹€.

ν•˜μ§€λ§Œ 단점은 κΈ°λŠ₯이 좔가될 λ•Œλ§ˆλ‹€ 클래슀 파일이 λŠ˜μ–΄λ‚˜ 관리가 νž˜λ“€μ–΄μ‘Œκ³ , 데이터λ₯Ό ν•œ λˆˆμ— 확인할 수 μžˆμ§€λ§Œ ν™•μΈν•˜κΈ° μœ„ν•΄μ„œλŠ” 직접 클래슀 νŒŒμΌμ„ 열어봐야 ν•œλ‹€λŠ” 점 μ΄μ—ˆλ‹€.

μš”μ²­ νŒŒλΌλ―Έν„°κ°€ λ§Žμ§€ μ•Šμ€ 경우 (2-3개) ꡳ이 클래슀 파일둜 λ”°λ‘œ μž‘μ„±ν•˜μ§€ μ•Šκ³  컨트둀러 λ©”μ„œλ“œμ— μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ λ§€ν•‘ν•˜λŠ” 것이 μ½”λ“œμ˜ 가독성과 μœ μ§€λ³΄μˆ˜ μΈ‘λ©΄μ—μ„œ 쒋은 방법이 될 것이라 μƒκ°λœλ‹€.



쑰회수 쀑볡 μΉ΄μš΄νŒ… 예방

κ²Œμ‹œκΈ€ 쑰회수 쀑볡 μΉ΄μš΄νŒ… μ˜ˆλ°©μ„ μœ„ν•΄ read_history ν…Œμ΄λΈ”μ„ 두어 κ΅¬ν˜„ν–ˆλ‹€.

Cookie 의 경우 ν•˜λ‚˜μ˜ 도메인 λ‹Ή μ‚¬μš©ν•  수 μžˆλŠ” κ°œμˆ˜κ°€ μ œν•œλ˜κΈ° λ•Œλ¬Έμ—, μ—¬λŸ¬ κ²Œμ‹œκΈ€μ„ μ‘°νšŒν•˜λ©΄ μ œν•œ 개수λ₯Ό λ„˜μ–΄λ²„λ¦΄ 것이라 μ˜ˆμƒν–ˆλ‹€. κ·Έλž˜μ„œ μ‚¬μš©μžκ°€ 이미 읽은 κ²Œμ‹œκΈ€μΈμ§€ 확인할 수 μžˆλ„λ‘ μ„œλ²„μ—μ„œ μ‚¬μš©μž 아이디 κ²Œμ‹œκΈ€ 쑰회 μ‹œκ°„ 을 두어 μ²΄ν¬ν–ˆλ‹€.

/* νŠΉμ • κΈ€ */
/* 쑰회수 μˆ˜μ • */
public BbsResponse getBbs(Integer seq, String readerId) {
// 둜그인 ν•œ μ‚¬μš©μžμ˜ 쑰회수만 μΉ΄μš΄νŒ…
if (!readerId.isEmpty()) {
CreateReadCountParam param = new CreateReadCountParam(seq, readerId);
Integer result = dao.createBbsReadCountHistory(param); // 쑰회수 νžˆμŠ€ν† λ¦¬ 처리 (insert: 1, update: 2)
if (result == 1) {
Integer updatedRecordCount = dao.increaseBbsReadCount(seq); // 쑰회수 증가
}
}
return new BbsResponse(dao.getBbs(seq));
}

μŠ€μΌ€μ₯΄λŸ¬λ₯Ό κ΅¬ν˜„ν•΄ 24μ‹œκ°„ λ‹¨μœ„λ‘œ 쑰회수λ₯Ό μ΄ˆκΈ°ν™” μ‹œμΌœμ„œ λ‹€μŒλ‚ μ΄ 되면 μ‘°νšŒμˆ˜κ°€ λ‹€μ‹œ μΉ΄μš΄νŒ… 될 수 μžˆκ²Œλ” κ΅¬ν˜„ν•˜λ©΄ 쒋을 것 κ°™λ‹€. μ•„λ‹ˆλ©΄ μ‘°νšŒμˆ˜λΌλŠ” 데이터λ₯Ό κ΄€λ¦¬ν•˜μ§€ μ•Šκ³  읽은 κΈ€, 읽지 μ•Šμ€ κΈ€ 둜 관리될 수 μžˆλ„λ‘ κ΅¬ν˜„ν•˜λŠ” 것도 ν•˜λ‚˜μ˜ 쒋은 방법이라 μƒκ°ν•œλ‹€.