개발환경

 

  1. 운영체제 : Mac OS
  2. 통합개발환경(IDE) : IntelliJ Ultimate
  3. JDK version : JDK 22
  4. Spring Boot version : 3.3.2
  5. Database : MySQL
  6. Build Tool : Maven
  7. Packaging : Jar

 

Dependencies
  • Spring Web
  • Spring Data JPA
  • MySQL Driver
  • H2 Database
  • Thymeleaf
  • Lombok
  • Spring Security
  • Spring Boot DevTools
  • querydsl
  • thymeleaf-layout-dialect
  • validation
  • thymeleaf-extras-springsecurity6

 

패키지 설계

 

스프링 시큐리티 설정


  • config 패키지에 securityconfig 클래스 생성

  
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(configurer ->
configurer
.requestMatchers("/h2-console/**").permitAll() // H2 콘솔에 대한 접근 허용
.anyRequest().authenticated() // 나머지 요청에 대해 인증 필요
);
// use HTTP Basic authentication
http.httpBasic(Customizer.withDefaults());
// disable Cross Site Request Forgery(CSRF)
http.csrf(csrf -> csrf.disable());
return http.build();
}
// H2 콘솔 관련 설정
@Bean
@ConditionalOnProperty(name = "spring.h2.console.enabled", havingValue = "true")
public WebSecurityCustomizer configureH2ConsoleEnable() {
return web -> web.ignoring()
.requestMatchers(PathRequest.toH2Console());
}

 

  • @Configuration : Spring의 구성 클래스를 나타내며, Bean 정의를 포함하는 클래스를 의미
  • @EnableWebSecurity : Spring Security를 활성화. Spring Security의 설정을 정의하고 적용
💡  BCryptPasswordEncoder
      비밀번호를 암호화하고 검증하는 데 사용되는 PasswordEncoder 구현체. BCrypt 알고리즘을 사용하여 비밀번호를 암호화한다. BCrypt는 비밀번호 해시를 생성하는 데 안전한 방법으로 널리 사용

 

 

SecurityFilterChain HTTP 요청에 대한 보안 필터 체인을 정의. HttpSecurity 객체를 사용하여 필터 체인을 설정.

HttpSecurity : HTTP 요청에 대한 보안 설정을 구성하는 데 사용됨

authorizeHttpRequests(configurer -> ...) : HTTP 요청에 대한 접근 권한을 설정

requestMatchers(...) : URL 패턴에 따라 요청을 필터링.

permitAll() : 해당 URL 패턴에 대해 인증 없이 접근을 허용.

anyRequest().authenticated() : 위에서 정의된 패턴을 제외한 모든 요청은 인증된 사용자만 접근할 수 있도록 설정.

httpBasic(Customizer.withDefaults()) : HTTP Basic Authentication을 활성화. 이 방법은 간단한 인증 방법으로 사용자 이름과 비밀번호를 HTTP 헤더에 포함시켜 인증을 수행.

csrf.disable() : Cross Site Request Forgery (CSRF) 보호를 비활성화. CSRF는 웹 애플리케이션에서 악의적인 요청을 방지하기 위한 보안 메커니즘. 일반적으로 비활성화하지 않는 것이 좋지만, API 또는 비상 상황에서는 비활성화할 수 있다.

 

https://wikidocs.net/162150

 

3-05 스프링 시큐리티란?

* `[완성 소스]` : [https://github.com/pahkey/sbb3/tree/v3.05](https://github.com/pahkey/sbb3/tree/v3.05…

wikidocs.net

 

📍 내가 노션에 정리한 내용!

https://www.notion.so/5-Spring-Boot-Security-b95a14467d6f4a87937a66b8d217c2d2?pvs=4

https://www.notion.so/8-Spring-MVC-Security-3fc33d56e52e4d809dac4b7fbdc0f025?pvs=4

 

 

.requestMatchers("/**").permitAll() - 일단 스프링 시큐리티 적용안하고 바로 화면 보이도록 설정...

 

 

엔티티, DTO 코드 설정


 

1. 일반 유저 / 관리자 역할 코드

  •  com.doraflower 패키지 아래 'constant' 키지 생성 - Role.java 생성
  •  enum(열거형)

 

https://velog.io/@mooh2jj/Java-Enum%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0

 

[Java] Enum을 사용하는 이유와 활용법

Java enum은 제한된 값 목록을 갖는 타입입니다. enum은 다음과 같은 이점을 갖습니다.enum은 컴파일 타임에 타입 안정성을 보장합니다. 특정 범위의 값만 사용 가능하므로 컴파일 오류나 런타임 예

velog.io

 


  
public enum Role {
USER, ADMIN
}

 

 

2. MemberFormDTO 클래스 코드 - 회원가입 화면으로부터 넘어오는 정보를 담는다.


  
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberFormDTO {
@NotBlank(message = "Name is required")
private String name;
@NotBlank(message = "Email is required")
@Pattern(regexp = "^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
message = "Please enter a valid email address.")
private String email;
@NotBlank(message = "Password is required")
@Length(min=8, max=16, message = "Enter a password between 8 and 16 characters, including numbers.")
private String password;
@NotBlank(message = "Address is required")
private String address;
}

 

2024.07.25 - [Spring & Spring Boot] - @Lombok 및 Entity 관련 어노테이션

 

  • 회원가입 폼에서 받을 내용 - 이름 | 이메일 주소 (로그인 시 확인) | 비밀번호 | 주소
  • message는 유효성 검사시 에러가 발생하면 출력될 내용!

 

3. Member 클래스 코드 - 회원 정보를 저장하는 엔티티 (DB에서 관리)


  
@Entity
@Table(name = "member")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id
@GeneratedValue (strategy = GenerationType.IDENTITY)
@Column(name="member_id")
private Long memberId;
@Column(nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String address;
@Enumerated(EnumType.STRING)
private Role role;
public static Member createMember(MemberFormDTO memberFormDTO,
PasswordEncoder passwordEncoder) {
String password = passwordEncoder.encode(memberFormDTO.getPassword());
Member member = Member.builder()
.name(memberFormDTO.getName())
.email(memberFormDTO.getEmail())
.password(password)
.address(memberFormDTO.getAddress())
.role(Role.USER)
.build();
return member;
}
}

 

  • 데이터베이스 테이블 이름은 'member'로 지정
  • 이메일을 통해 회원 구분을 위해 'unique'설정
  • Role은 string으로 저장하도록 설정
  • Member 엔티티 생성하는 메소드 설정 - 파라미터로 dto와 passwordencoder를 받음
  • 패스워드는 db에 암호화 돼서 저장됨
  • role은 'USER'로 설정해줌 -> ADMIN 먼저 만들고 바꿔줘도 됨

 

 

Repository 코드 설정 - DAO 역할


  • repository 패키지 아래 MemberRepository 인터페이스 생성

  
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByEmail(String email);
}
  • 회원가입 시 중복된 회원이 있는지 검사하기 위해 이메일로 회원을 검사할 수 있도록 쿼리 메소드 작성

 

 

Service 코드 설정


  • service 패키지 아래 MemberService 인터페이스, MemberServiceImpl 클래스 생성
특성 서비스 계층이 필요한 경우 서비스 계층이 필요없는 경우
비즈니스 로직의 복잡성 복잡한 비즈니스 로직이 있는 경우 비즈니스 로직이 거의 없거나 단순한 경우
유지보수 비즈니스 로직을 중앙화하여 유지 보수 용이 간단한 애플리케이션에서 유지 보수가 덜 필요
테스트 용이성 독립적인 테스트가 필요한 경우 테스트가 크게 중요하지 않은 경우
트랙잭션 관리 트랜잭션 관리를 필요로 하는 경우 트랜잭션 관리가 필요 없는 경우
계층화된 아키텍처 MVC 패턴 등 계층화된 아키텍처를 선호하는 경우 단일 클래스 또는 구조화된 아키텍처가 불필요

 


  
public interface MemberService {
Member saveMember(Member theMember);
}

  
@Service
@Log4j2
@RequiredArgsConstructor
@Transactional
public class MemberServiceImpl implements MemberService, UserDetailsService {
private final MemberRepository memberRepository;
@Override
public Member saveMember(Member theMember) {
Member tempMember = memberRepository.findByEmail(theMember.getEmail());
if (tempMember != null) {
throw new IllegalStateException("Already Registered Member!");
}
return memberRepository.save(theMember);
}
}

 

특성 단일 클래스 인터페이스 + 구현체
구조 단순하지만 커질 수 있음 명확하게 구조화됨
유지보수 어려울 수 있음 용이함
테스트 어려울 수 있음 용이함 (모킹 가능)
확장성 낮음 높음
의존성 주입 어려울 수 있음 쉬움

 

 

  • @Autowired는 빈에 생성자가 1개이고 생성자의 파라미터 타입이 빈으로 등록 가능하면 사용 안해도 의존성 주입 가능..
  • 이미 가입된 회원은 IllegalStateException 예외 발생

 

회원 가입 기능 테스트


  • 마우스 오른쪽 버튼 - Go To - Test 자동생성 기능 사용

  
@SpringBootTest
@Transactional
@Log4j2
@RequiredArgsConstructor
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberServiceImplTest {
@Autowired
MemberService memberService;
@Autowired
PasswordEncoder passwordEncoder;
@Test
@DisplayName("회원가입 테스트")
public void saveMemberTest() {
MemberFormDTO theMemberDTO = MemberFormDTO.builder()
.email("test@email.com")
.name("Dora")
.address("Daegu")
.password("1234")
.build();
// 비밀번호 인코딩 및 회원 생성
Member member = Member.createMember(theMemberDTO, passwordEncoder);
// 회원 저장
Member savedMember = memberService.saveMember(member);
// 로그로 저장된 Member 확인
log.info("Saved Member: {}", savedMember);
assertNotNull(savedMember.getMemberId()); // ID가 null이 아닌지 확인
assertEquals("test@email.com", savedMember.getEmail());
}
@Test
@DisplayName("중복 회원 가입 테스트")
public void saveDuplicateMember() {
// 객체 생성
Member member1 = new Member(null, "Dora", "test@email.com", "1234", "Daegu", ADMIN);
Member member2 = new Member(null, "Dora", "test@email.com", "1234", "Daegu", USER);
// 객체 저장
memberService.saveMember(member1);
// 예외 처리
IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> {
memberService.saveMember(member2);
});
assertTrue(thrown.getMessage().contains("Already Registered Member!"));
}
}

 

  • h2 db에 테스트 할 거기 때문에 기존 properties 외 새로 하나 더 만들어서 경로를 지정해줬다.
  • Junit의 Assertions 클래스의 assertEquals 메소드를 이용해서 저장하려고 요청했던 값과 실제 저장된 데이터를 비교함.
  • assertThrows 메소드 이용하면 예외 처리 테스트가 가능.

https://junit.org/junit5/docs/5.0.1/api/org/junit/jupiter/api/Assertions.html

 

Assertions (JUnit 5.0.1 API)

Asserts that all supplied executables do not throw exceptions. If any supplied Executable throws an exception (i.e., a Throwable or any subclass thereof), all remaining executables will still be executed, and all exceptions will be aggregated and reported

junit.org

 

https://velog.io/@roycewon/JUnit-Assert-Methods1

 

JUnit - Assert Methods(1)

JUnit 5 모듈인 Jupiter는 JUnit4에 있는 Assertion method를 포함하여 여러 메소드를 제공한다. Java8에서 추가된 람다와 함께 사용하면 좋다.Assert method는 org.junit.jupiter.api.Assertions 라는 클래

velog.io

 

 

 

Controller 코드 구현


  • controller 패키지 아래 MemberController 생성

  
@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final PasswordEncoder passwordEncoder;
@GetMapping("/new")
public String memberForm(Model model) {
model.addAttribute("memberFormDTO", new MemberFormDTO());
return "member/memberForm";
}
@PostMapping("/new")
public String memberForm(@Valid MemberFormDTO memberFormDTO,
BindingResult bindingResult,
Model model) {
// 유효성 오류시 실행할 메서드
if (bindingResult.hasErrors()) {
System.out.println("Binding results: " + bindingResult.toString());
return "member/memberForm";
}
// 이메일 중복 시 예외처리
try {
Member member = Member.createMember(memberFormDTO, passwordEncoder);
memberService.saveMember(member);
} catch (IllegalStateException e){
model.addAttribute("errorMessage", e.getMessage());
return "member/memberForm";
}
// 성공 시 리다이렉트
return "redirect:/";
}
}

 

  • ModelmemberFormDTO라는 이름으로 새로운 MemberFormDTO 객체를 추가. 뷰에서 이 객체를 사용하여 폼을 렌더링할 수 있다.
  • localhost:8080/members/new 접속하면 member 패키지 아래에 있는 memberForm.html 화면이 출력됨.
  • @PostMapping은 form 을 제출했을 때 실행되는 코드~ 페이지 구현하고 작성해도 됨!

 

 

회원가입 페이지 구현


  • templates 패키지 아래 member 패키지 생성 - memberForm.html 생성
  • Thymeleaf 템플릿 사용해서 작성함.
  • fragments 패키지 아래에 header/footer html 만들어서 layout01.html에 fragment로 설정하고 content영역만 따로 페이지 구현해서 연동되도록 만들었습니닷,

2024.05.09 - [Spring & Spring Boot] - [Spring Boot] Thymeleaf

https://github.com/SominY/Dora-Flower-Shop/blob/main/src/main/resources/templates/member/memberForm.html(소스코드)

 

Dora-Flower-Shop/src/main/resources/templates/member/memberForm.html at main · SominY/Dora-Flower-Shop

쇼핑몰 프로젝트 (개인 프로젝트). Contribute to SominY/Dora-Flower-Shop development by creating an account on GitHub.

github.com

 

 

 

 

 

 

 

내용 참고: 책 '스프링 부트 쇼핑몰 프로젝트 with JPA'


JpaRepositoy 지원 메소드

메소드 기능
<S extends T> save(S entity) 엔티티 저장 및 수정
void delete(T entity) 엔티티 삭제
count() 엔티티 총 개수 반환
Iterable<T> findAll() 모든 엔티티 조히

 

📍 연관 페이지

https://www.notion.so/4-Spring-Boot-Rest-CRUD-b666e9cbc99746048343b5ac0b464871?pvs=4

2024.05.25 - [Spring & Spring Boot] - [Spring Boot] Spring Data JPA (1)

 

[Spring Boot] Spring Data JPA (1)

1.  Spring Data JPAJDBC에서 작업하다가 MyBatis를 배울 경우에는 SQL 작성법도 거의 동일하고, 코드의 간략화가 목표여서 러닝커브가 높지 않지만JPA의 경우에는 JDBC, MyBatis와는 사용방법이 상당한

allriver.tistory.com

 


Query method & JPQL snippet

Keyword Sample JPQL snippet
And findByLastnameAndFirstname ... where x.lastname =  ?1 and x.firstname = ?2
Or findByLastnameOrFirstname ... where x.lastname =  ?1 or x.firstname = ?2
Is, Equals findByFirstname
findByFirstnameIs
findByFirstnameEquals
... where x.firstname = ?1
Between findByStartDateBetween ... where x.startDate between ?1 and ?2
LessThan findByAgeLessThan ... where x.age < ?1
LessThanEqual findByAgeLessThanEqual ... where x.age <= ?1
GreaterThan findByAgeGreaterThan ... where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual ... where x.age >= ?1
After findByStartDateAfter ... where x.startDate > ?1
Before findByStartDateBefore ... where x.startDate < ?1
IsNull, Null
IsNotNull
findByAge(Is)Null ... where x.age is null
NotNull findByAge(Is)NotNull ... where x.age not null
Like findByAgeFirstnameLike ... where x.firstname like ?1
NotLike findByAgeFirstnameNotLike ... where x.firstname not like ?1
StartingWith findByFirstnameStartingWith ... where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith ... where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining ... where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc ... where x.age ?1 order by x.lastname desc
Not findByLastnameNot ... where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) ... where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) ... where x.age not in ?1
True findByActiveTrue() ... where x.active = true
False findByActiveFalse() ... where x.active = false
IgnoreCase findBYFirstnameIgnoreCase ... where UPPER(x.firstname) = UPPER(?1)

 

 

 


 

javax.validation 어노테이션 예시

어노테이션 설명
@NotEmpty NULL 체크 및 문자열의 경우 길이 0인지 검사
@NotBlank NULL 체크 및 문자열의 경우 길이 0 및 빈 문자열(" ") 검사
@Length(min=, max=) 최소, 최대 길이 검사
@Email 이메일 형식인지 검사
@Max(숫자) 지정한 값보다 작은지 검사
@Min(숫자) 지정한 값보다 큰지 검사
@Null 값이 NULL인지 검사
@NotNull 값이 NULL이 아닌지 검사

 

 

 

 

 

 

* 내용 참고 : 책 '스프링부트 쇼핑몰 프로젝트 with JPA'


1.  application-test.properties 파일 생성


  
# datasource 설정
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:./data/demo
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# H2 데이터베이스 방언 설정
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

 

- username, password 는 기본값으로 설정해줘도 됨

- port 는 기본값이고 path를 지정해주면 localhost:8080/h2-console 로 접속하면됨

- '인메모리 모드' 에서는 url 설정을 jdbc:h2:mem:{db이름} 으로 설정

- 'embedded mode' 에서는 url 설정을 jdbc:h2:{db가 저장될 경로} 로 설정

 

2. Spring Security 적용 중이라면 h2 database 접속시 설정이 안 되도록 코드 입력


  
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@ConditionalOnProperty(name = "spring.h2.console.enabled",havingValue = "true")
public WebSecurityCustomizer configureH2ConsoleEnable() {
return web -> web.ignoring()
.requestMatchers(PathRequest.toH2Console());
}
}

 

- @ConditionalOnProperty: 이 애너테이션은 특정 프로퍼티가 설정된 경우에만 해당 빈을 생성. 즉, spring.h2.console.enabled가 true로 설정된 경우에만 configureH2ConsoleEnable() 메서드에서 반환한 빈이 생성됨.

 

- WebSecurityCustomizer: 이 빈은 WebSecurityCustomizer 타입의 빈을 생성하고, 이를 사용하여 특정 URL 패턴에 대해 보안 필터를 무시하도록 설정함. PathRequest.toH2Console()을 사용하여 H2 콘솔의 URL 패턴을 정의하고, 이 패턴에 대해 보안 필터를 무시하게 설정.

 

3. 웹 브라우저 창에서 localhost:8080/h2-console 접속

- properties에서 설정한 driver class, jdbc url, username, password 입력 후 test connection 클릭

- 그 후 connect 클릭하면 쿼리 작성할 수 있는 창이 뜨고, 결과확인 코드를 입력해서 RUN 클릭하면 됨!

 

* 테스트 코드 결과물은 어플리케이션이 종료되면 없어짐!

 

 

내용 참고:

https://dukcode.github.io/spring/h2-console-with-spring-security/

 

Spring Security에서 H2 Console 사용하기

Spring Security에서 H2 Console 사용하기

dukcode.github.io

https://colabear754.tistory.com/193

 

[Spring Boot] H2 DB Embedded Mode 사용하기

목차 들어가기 전에 H2는 굉장히 작고 가벼운 RDBMS로, 그 특성상 제공되는 기능은 제한적이지만 속도가 빠르고 별도의 프로그램 없이 웹브라우저 기반의 DB 콘솔을 사용할 수 있다는 장점도 있다.

colabear754.tistory.com

 


@Lombok 

🚀 Lombok 라이브러리는 Getter/Setter, ToString 과 같은 반복적인 자바 코드를 컴파일할 때 자동으로 생성해주는 라이브러리

 

어노테이션 설명
@Getter/Setter 코드를 컴파일할 때 속성들에 대한 Getter/Setter 메소드 작성
@ToString toString() 메소드 작성
@ToString(exclude={”변수명”} 원하지 않는 속성 제외한 toString()메소드 작성
@NonNull 해당 변수가 null 체크, NullPointerException 예외 발생
@EqualsAndHashCode equals()와 hashCode() 메소드 작성
@Builder 빌더 패턴을 이용한 객체 생성
@NoArgsConstructor 파라미터가 없는 기본 생성자 생성
@AllArgsConstructor 모든 속성에 대한 생성자 생성
@RequiredArgsConstructor 초기화되지 않은 Final, @NonNull 어노테이션이 붙은 필드에 대한 생성자 생성
@Log log 변수 자동 생성
@Value 불변(immutable) 클래스 생성
@Data @ToString, @EqualsAndHashCode, @Getter, @Setter, @RequiredArgsConstructor를 합친 어노테이션

 


Entity Mapping 관련 어노테이션

어노테이션 설명
@Entity 클래스를 엔티티로 선언 (JPA에 엔티티 클래스라는 것을 알려줌)
@Table 엔티티와 매핑할 테이블을 지정
@Id 테이블의 기본키에 사용할 속성을 지정
@GeneratedValue 키 값을 생성하는 전략 명시
@Column 필드와 컬럼 매핑
@Lob BLOB, CLOB 타입 매핑
@CreationTimestamp insert 시 시간 자동 저장
@UpdateTimestamp update 시 시간 자동 저장
@Enumerated enum 타입 매핑
@Transient 해당 필드 데이터베이스 매핑 무시
@Temporal 날짜 타입 매핑
@CreateDate 엔티티가 생성되어 저장될 때 시간 자동 저장
@LastModifiedDate 조회한 엔티티의 값을 변경할 때 시간 자동 저장

 

💡 CLOB과 BLOB 의미
     CLOB 이란 사이즈가 큰 데이터를 외부 파일로 저장하기 위한 데이터 타입. 문자형 대용량 파일을 저장하는데 사용
     BLOB 이란 바이너리 데이터를 DB 외부에 저장하기 위한 타입. 이미지, 사운드, 비디오 같은 멀티미디어 데이터를 다룰 때 사용

 


@Column 속성

속성 설명 기본값
name 필드와 매핑할 컬럼의 이름 설정 객체의 필드 이름
unique(DDL) 유니크 제약 조건 설정  
insertable insert 가능 여부 true
updatable update 가능 여부 true
length String 타입의 문자 길이 제약조건 설정 255
nullable(DDL) null 값의 허용 여부 설정. false 설정 시 DDL 생성 시에 not null 제약 조건 추가  
columnDefinition 데이터베이스 컬럼 정보 직접 기술

ex.
@Column(columnDefinition = "varchar(5) default'10' not null")
 
precision, scale(DDL) BigDecimal 타입에서 사용(BigInteger 가능) precision은 소수점을 포함한 전체 자리수이고, scale은 소수점 자리수.
Double과 float 타입에는 적용되지 않음.
 

 

💡 DDL(Data Definition Language)
     테이블, 스키마, 인덱스, 뷰, 도메인을 정의, 변경, 제거할 때 사용하는 언어.
     가령, 테이블을 생성하거나 삭제하는 CREATE, DROP 등이 이에 해당

 


@GeneratedValue

🚀  Entity 클래스는 반드시 기본키를 가져야 함

생성 전략 설명
GenerationType.AUTO(default) JPA 구현체가 자동으로 생성 전략 결정
GenerationType.IDENTITY 기본키 생성을 DB에 위임
ex. MySQL 의 경우 AUTO_INCREMENT를 사용하여 기본키 생성
GenerationType.SEQUENCE DB 시퀀스 오브젝트를 이용한 기본키 생성
@SequenceGenerator를 사용하여 시퀀스 등록 필요
GenerationType.TABLE 키 생성용 테이블 사용. @TableGenerator 필요

 

 

📍   관련글    

2024.02.24 - [Database/MySQL] - [MySQL] 제약조건 | 식별자, 기본키, 복합키, 유니크, 체크

2024.02.24 - [Database/MySQL] - [MySQL] 제약조건 | 일련번호, 시퀀스, AUTO_INCREMENT

 

 

 

 

 

 

* 내용 참고 : 책 '스프링부트 쇼핑몰 프로젝트 with JPA'


1.  댓글 등록

댓글 추가는 모달창을 이용해서 처리
  ⚡️  모달창을 이용해서 새로운 댓글이 필요한 replyText나 replyWriter를 입력하도록 구성하고, 자바스크립트의 객체로 POST 호출하도록 구성

reply.js에 새로운 댓글을 등록하는 기능을 추가


  📍 파라미터를 JS의 객체로 받아서 axios.post()를 이용해서 전달해 줌


  
async function addReply(replyObj) {
const response = await axios.post(`/api/replies/`, replyObj);
return response;
}

 

  ✓  addReply()가 정상적으로 처리되면 서버에서는 '{'rno':11}'과 같은 JSON 데이터를 전송하게 됨
  ✓  이를 이용해서 댓글이 추가되면 경고창을 보여주고 마지막 페이지로 이동해서 등록된 댓글을 볼 수 있게 구성

 

read.html에는 댓글 등록을 위한 모달창을 <div layout:fragment="content">가 끝나기 전에 추가

  
<!-- register modal -->
<div class="modal registerModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Register Reply</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="input-group mb-3">
<span class="input-group-text">Reply Text</span>
<input type="text" class="form-control replyText" >
</div>
<div class="input-group mb-3">
<span class="input-group-text">Reply Writer</span>
<input type="text" class="form-control replyWriter" >
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary registerBtn">Register</button>
<button type="button" class="btn btn-outline-dark closeRegisterBtn" >Close</button>
</div>
</div>
</div>
</div>
<!-- // register modal -->

 

  ✓  모달창의 class 속성값은 registerModal이라고 지정하고 <input> 태그들은 replyText와 replyWriter 속성값을 지정
  ✓  모달창의 버튼들도 구분하기 위해서 class 속성값을 registerBtn, closeRegisterBtn 등으로 지정해서 사용

 

read.html의 <script> 부분에는 자주 사용하는 DOM 객체들을 미리 변수로 처리

  
// 등록 작업 관련
const registerModal = new bootstrap.Modal(document.querySelector('.registerModal'));
const registerBtn = document.querySelector('.registerBtn');
const replyText = document.querySelector('.replyText');
const replyWriter = document.querySelector('.replyWriter');
const closeRegisterBtn = document.querySelector('.closeRegisterBtn');

 

ADD REPLY 버튼을 눌렀을 때 모달창을 보여주도록 이벤트 처리와 모달창의 Close 버튼에 대한 처리

  
document.querySelector('.addReplyBtn').addEventListener('click', function (e) {
registerModal.show();
});
closeRegisterBtn.addEventListener('click', function (e) {
registerModal.hide();
});

 

모달창 오른쪽 하단의 Register 버튼을 눌렀을 때 이벤트 처리를 추가

 

  📍  reply.js의 addReply()를 호출하고 경고창을 통해서 추가된 댓글의 번호를 보여줌
  📍  경고창이 닫히면 마지막 페이지를 다시 호출해서 등록된 댓글이 화면에 보일 수 있도록 구성


  
registerBtn.addEventListener('click', function (e) {
// 매개변수로 사용할 객체 생성
const replyObj = { bno: bno, replyText: replyText.value, replyWriter: replyWriter.value }
addReply(replyObj).then(result => { // 등록이 된 후 결과처리
alert(result.data.rno);
registerModal.hide();
replyText.value = '';
replyWriter.value = '';
printReplies(1, 10, true); // 댓글 목록 갱신
}).catch(e => {
alert('Exception');
});
});


2.  댓글 페이지 번호 클릭

새로운 댓글이 추가되면 자동으로 마지막 페이지로 이동하기는 하지만, 댓글의 페이지 번호를 누를 때도 이동할 수 있으므로 수정 / 삭제 전에 페이지 이동 처리는 먼저 진행
  ⚡️  화면에서 페이지 번호를 구성하는 부분은 <li> 태그 내에 존재하는 <a> 태그이고 페이지 번호가 'data-page' 속성값으로 지정되어 있음

  ⚡️  페이지 번호는 매번 새로이 구성하므로 이벤트 처리할 때는 항상 고정되어 있는 <ul>을 대상으로 이벤트 리스너를 등록하는 방식을 이용

 

read.html에 코드 추가

  
// 3. 페이징 클릭
let page = 1;
let size = 10;
replyPaging.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const target = e.target;
if (!target || target.tagName !== 'A') {
return;
}
page = target.getAttribute('data-page');
printReplies(page, size);
});

 

  ✓  page와 size를 별도의 변수로 처리하는 것은 나중에 댓글 수정과 같은 작업에서 현재 페이지 번호를 유지 해야 할 가능성이 있기 때문
  ✓  이벤트 처리가 완료되면 댓글의 페이지 이동이 가능해 짐

 


3.  댓글 조회와 수정

댓글을 조회한다는 것은 댓글을 수정하거나 삭제하기 위함
댓글 조회는 등록과 유사하게 모달창을 이용해서 수정이나 삭제가 가능한 버튼들을 보여주는 형태로 구성

 

1)  Axios 통신 부분

  👾  reply.js에는 특정한 번호의 댓글을 조회하고 수정할 수 있는 기능을 구성
  👾  댓글 조회는 GET 방식으로 처리되고, 댓글 수정은 PUT 방식으로 호출


  
async function getReply(rno) {
const response = await axios.get(`/api/replies/${rno}`);
return response.data;
}
async function modifyReply(replyObj) {
const response = await axios.put(`/api/replies/${replyObj.rno}`, replyObj);
return response.data;
}

 

2)  read.html의 모달창 처리

  👾  댓글 수정 후에는 다시 현재 페이지를 호출해서 최대한 목록에서 수정된 댓글을 확인할 수 있도록 함

 

read.html에 등록 모달창 아래 영역을 새롭게 추가

  
<!-- modify modal -->
<div class="modal modifyModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title replyHeader"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="input-group mb-3">
<span class="input-group-text">Reply Text</span>
<input type="text" class="form-control modifyText" >
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-info modifyBtn">Modify</button>
<button type="button" class="btn btn-danger removeBtn">Remove</button>
<button type="button" class="btn btn-outline-dark closeModifyBtn">Close</button>
</div>
</div>
</div>
</div>
<!-- // modify modal -->

 

  ✓  <div class="modifyModal">의 경우 class 속성값이 replyHeader 영역을 이용해서 선택한 댓글의 번호를 보여주도록 하고, modifyText 부분에는 댓글의 내용을 수정할 수 있도록 구성

 

read.html에 변수들을 추가해서 제어가 가능하도록 구성

  
const modifyModal = new bootstrap.Modal(document.querySelector('.modifyModal'));
const replyHeader = document.querySelector('.replyHeader');
const modifyText = document.querySelector('.modifyText');
const modifyBtn = document.querySelector('.modifyBtn');
const removeBtn = document.querySelector('.removeBtn');
const closeModifyBtn = document.querySelector('.closeModifyBtn');

 

특정한 댓글을 눌렀을 때 모달창을 보여주도록 replyList에 이벤트 처리를 추가

 

  📍  댓글 목록 역시 매번 브라우저에서 새로 만들어지기 때문에 이벤트는 항상 존재하는 바깥쪽 <ul>을 대상으로 처리


  
// 댓글 내용을 클릭했을 때 수정 모달창 띄우기
replyList.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const target = e.target;
if (!target || target.tagName !== 'SPAN') {
return;
}
const rno = target.getAttribute('data-rno');
if (!rno) {
return;
}
getReply(rno).then(reply => { // 댓글의 내용을 모달창에 채워서 보여줌
console.log(reply)
replyHeader.innerHTML = reply.rno;
modifyText.value = reply.replyText;
modifyModal.show();
}).catch(e => alert('error'));
});

 


3)  댓글 수정과 화면 갱신

  👾  댓글 수정은 화면에 있는 댓글의 내용과 번호, 게시물의 번호를 이용해서 처리할 수 있음
  👾  신경써야 할 부분은 댓글의 수정 후 처리.

         ➡️  수정된 댓글은 결국 목록에서 확인하게 되기 때문에 만일 사용자가 댓글을 수정하는 사이에 댓글이 많이 추가되면 확인할 방법이 없음

  👾  최대한 자신이 보고 있었던 페이지를 유지하는 수준으로 구현하도록 함

 

화면에서 Modify 버튼에 대한 이벤트 처리와 Close 버튼의 이벤트 처리를 작성

  
// 모달창의 수정 버튼 클릭
modifyBtn.addEventListener('click', function (e) {
console.log('modifyBtn... click');
const replyObj = {bno: bno, rno: replyHeader.innerHTML, replyText: modifyText.value};
modifyReply(replyObj).then(result => {// 수정 처리 후
alert(result.rno + '댓글이 수정되었습니다.');
modifyText.value = '';
modifyModal.hide();
printReplies(page, size);
}).catch(e => {
console.log(e)
});
});
// 모달창 close 버튼 클릭
closeModifyBtn.addEventListener('click', function () {
modifyModal.hide();
});

 

  ✓  댓글이 수정된 후에는 다시 현재 페이지를 볼 수 있도록 printReplies()를 호출하므로 수정한 후에 현재 페이지를 다시 갱신


4)  댓글 삭제

reply.js에 삭제 처리 추가

  
async function removeReply(rno) {
const response = await axios.delete(`/api/replies/${rno}`);
return response.data;
}

 

read.html에는 모달 창의 Remove 버튼을 눌러서 댓글을 삭제

 

  📍 삭제 후에는 경고창을 통해서 댓글이 삭제되었음을 알려주고 댓글의 1페이지로 이동


  
/*
5. 삭제 관련
*/
removeBtn.addEventListener('click', function (e) {
removeReply(replyHeader.innerHTML).then(result => {
alert(result.rno + ' 댓글이 삭제되었습니다.');
modifyText.value = '';
modifyModal.hide();
page = 1; // 해당 댓글이 삭제되었으니 1페이지로 이동
printReplies(page, size);
}).catch(e => {
console.log(e)
});
});

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  비동기 처리와 Axios

대부분 프로그래밍의 시작은 항상 동기화된 방식을 이용.

result1 = doA();
result2 = doB(result1);
result3 = doC(result2);


  ⚡️  흔하게 볼 수 있는 동기화된 코드인데 doA()를 실행해서 나온 결과로 result1을 이용해서 doB()를 호출하는 방식
  ⚡️  코드는 doA() -> doB() -> doC()의 순서대로 호출됨

  📍  동기화된 방식의 단점은 doA()의 실행이 완료되어야만 doB()의 실행이 가능함. 즉 doA()의 결과를 반환할 때까지 다른 작업은 실행되지 않기 때문에 동시에 여러 작업을 처리할 수없음

  📍  비동기 방식은 커피 가게에 여러 명의 점원이 있는 상황과 유사. 점원이 여러 명이면 한 명은 주문을 계속 받고, 다른 점원은 계속해서 커피를 제조할 수 있음. 
    ➡️  비동기 방식의 핵심은 '통보'. 비동기는 여러 작업을 처리하기 때문에 나중에 결과가 나오면 이를 '통보'해 주는 방식을 이용.
         이러한 방식을 전문용어로는 콜백 callback이라고 함.

  📍  비동기 방식은 'doA()'를 호출할 때 doB()를 해 줄 것을 같이 파라미터로 전달

function doA(callback) {
...
result1 = ....
callback(result1)
}


  ⚡️  파라미터로 전달되는 콜백을 내부에서 호출하는 코드

 

  📍 자바 스크립트에서 함수는 '일급 객체 first-class object'로 일반 객체와 동일한 위상을 가지고 있으므로 파라미터가 되거나 리턴타입이 될 수 있음
  📍  비동기 방식에서 콜백을 이용하는 것이 해결책이 되기는 하지만 동기화된 코드에 익숙한 개발자들에게는 조금만 단계가 많아져도 복잡한 코드를 만들어야 하는 불편함이 있음
    ➡️  자바 스크립트에서는 Promise하는 개념을 도입해서 '비동기 호출을 동기화된 방식'으로 작성할 수 있는 문법적인 장치가 있는데 Axios는 이를 활용하는 라이브러리
    ➡️  Axios를 이용하면 Ajax를 호출하는 코드를 작성할 때 마치 동기화된 방식으로 작성할 수 있어서 자바 스크립트를 기반으로 하는 프레임워크 (Angula)나 라이브러리 (React, Vue)에서 많이 사용

 

시작하기 | Axios Docs

시작하기 브라우저와 node.js에서 사용할 수 있는 Promise 기반 HTTP 클라이언트 라이브러리 Axios란? Axios는 node.js와 브라우저를 위한 Promise 기반 HTTP 클라이언트 입니다. 그것은 동형 입니다(동일한 코

axios-http.com

 


2.  Axios를 위한 준비

Axios를 활용해 Ajax를 이용하기 위해서는 댓글 처리가 필요한 화면에 Axios 라이브러리를 추가해주어야 함
자바 스크립트 코드의 경우 read.html에서는 주로 이벤트 관련된 부분을 처리하도록 하고 별도의 JS 파일을 작성해서 Axios를 이용하는 통신을 처리하도록 구성

static 폴더에 있는 js 폴더에 reply.js 파일을 추가
  • read.html의 <div layout:fragment="content">가 끝나기 전에 Axios 라이브러리를 추가 하고 reply.js 파일도 같이 추가

  
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js">
</script>
<script src="/js/reply.js"></script>

 

read.html에 댓글과 관련된 화면 구성을 위한 코드 추가
  • 댓글 작성 버튼
  • 댓글 목록
  • 댓글 목록 페이징

  
<div class="row mt-3">
<div class="col-md-12">
<div class="my-4">
<!-- 댓글 작성 버튼 -->
<button class="btn btn-info addReplyBtn">ADD REPLY</button>
</div>
<!-- 댓글 목록 -->
<ul class="list-group replyList">
</ul>
</div>
<div class="row mt-3">
<div class="col">
<!-- 댓글 목록 페이징 -->
<ul class="pagination replyPaging">
</ul>
</div>
</div>
</div>

 


1)  Axios 호출해 보기

  👾  reply.js에 간단하게 Axios를 이용하는 코드를 추가. Axios를 이용할 때 async / await를 같이 이용하면 비동기 처리를 동기화된 코드처럼 작성할 수 있음
      ⚡️  async는 함수 선언 시에 사용하는 데 해당 함수가 비동기 처리를 위한 함수라는 것을 명시하기 위해서 사용하고,
            await는 async 함수 내에서 비동기 호츨하는 부분에 사용


  
async function get1(bno) {
const result = await axios.get(`/api/replies/list/${bno}`);
console.log(result);
}

 

read.html에 get1()을 호출하는 코드를 작성

  
<script layout:fragment="script" th:inline="javascript">
const bno = [[${dto.bno}]];
get1(bno);
</script>

 

브라우저에서 댓글이 많이 달린 조회 페이지로 이동

 

 

A.  비동기 함수의 반환

 

  ✓  화면에서 결과가 필요하다면 Axios의 호출 결과를 반환받아야 하기 때문에 reply.js에서 코드 수정


  
async function get1(bno) {
const result = await axios.get(`/api/replies/list/${bno}`);
//console.log(result);
return result.data;
}

  
<script layout:fragment="script" th:inline="javascript">
const bno = [[${dto.bno}]];
console.log(get1(bno));
</script>

 


3.  댓글 처리와 자바스크립트

1)  댓글 목록 처리

  👾  가장 먼저 개발하려는 기능은 댓글 목록 기능
  👾  댓글도 페이징 처리가 가능하도록 구성

reply.js에서 개발하려는 함수의 이름은 getList()라 하고, 파라미터는 다음과 같이 결정
  • bno : 현재 게시물 번호
  • page : 페이지 번호
  • size : 페이지당 사이즈
  • goLast : 마지막 페이지 호출 여부

    ⚡️  이 중에서 goLast는 조금 특별한 용도로 사용. 댓글의 경우 한 페이지에서 모든 동작이 이루어지므로 새로운 댓글이 등록되어도 화면에는 아무런 변화가 없다는 문제가 생김
    ⚡️  또한 페이징 처리가 되면 새로 등록된 댓글이 마지막 페이지에 있기 때문에 댓글의 결과를 볼 수 있다는 문제가 생김

    📍  goLast변수를 이용해서 강제적으로 마지막 댓글 페이지를 호출하도록 함

reply.js에 getList() 함수를 작성

  
async function getList({bno, page, size, goLast}) {
const result = await axios.get(`/api/replies/list/${bno}?page=${page}`, {params: {page, size}})
return result.data
}

 

read.html에는 getList()를 호출하는 함수와 현재 페이지가 로딩되면 해당 함수를 호출하도록 작성

  
function printReplies(page, size, goLast) {
getList({bno, page, size, goLast}).then(
data => {console.log(data);}
).catch(e => {
console.error();
});
}
printReplies(1, 10); // 무조건 호출

 

  👾  결과 데이터는 dtoList로 화면에 목록(replyList)을 처리하고, 나머지 정보들로 페이지 번호를 출력

 

read.html에는 댓글 목록을 출력하는 printList()와 페이지 번호를 출력하는 printPages() 함수를 작성하고 Axios의 결과를 출력하도록 수정

  
const bno = [[${dto.bno}]];
// console.log(get1(bno));
function printReplies(page, size, goLast) {
getList({bno, page, size, goLast}).then(
data => {
console.log(data);
printList(data.list); // 목록 처리
printPages(data); // 페이지 처리
}
).catch(e => {
console.error();
});
}
printReplies(1, 10); // 무조건 호출
const replyList = document.querySelector('.replyList'); // 댓글 목록 DOM
const replyPaging = document.querySelector('.replyPaging'); // 페이지 목록 DOM
function printList(dtoList) { // 댓글 목록 출력
let str = '';
if(dtoList && dtoList.length > 0) {
for (const dto of dtoList) {
str += ` <li class="list-group-item d-flex replyItem">
<span class="col-2">${dto.rno}</span>
<span class="col-6" data-rno="${dto.rno}">${dto.replyText}</span>
<span class="col-2">${dto.replyWriter}</span>
<span class="col-2">${dto.regDate}</span>
</li>`;
}
}
replyList.innerHTML = str;
}
function printPages(data) { // 페이지 목록 출력
// pagination
page = data.page;
let pageStr = '';
if(data.prev) {
pageStr += `<li class="page-item">
<a class="page-link" data-page="${data.start - 1}">PREV</a></li>`;
}
for(let i = data.start; i <= data.end; i++) {
pageStr += `<li class="page-item ${i === data.page ? "active" : ""}">
<a class="page-link" data-page="${i}">${i}</a></li>`;
}
if(data.next) {
pageStr += `<li class="page-item">
<a class="page-link" data-page="${data.end + 1}">NEXT</a></li>`;
}
// console.log(pageStr);
replyPaging.innerHTML = pageStr;
}

 


A. @JsonFormat, @JsonIgnore


  📍 출력된 댓글의 모양을 보면 등록 시간 regDate 부분이 배열로 처리되어서 지저분해 보임. 

         ➡️  RelyDTO에 @JsonFormat을 이용해서 JSON 처리 시에 포맷팅을 지정
  📍 댓글 수정 시간 modDate의 경우 화면에서 전혀 출력할 일이 없으므로 JSON 으로 변환될 때 제외하도록 @JsonIgnore를 적용


  
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime regDate;
@JsonIgnore
private LocalDateTime modDate;

 


B. 마지막 페이지로 이동


  📍 댓글 페이징은 새로 글이 추가되는 상황이 발생하면 마지막으로 등록되기 때문에 확인이 어려운 문제가 있음
        ➡️  이를 처리하려면 댓글 목록 데이터의 total을 이용해서 다시 마지막 페이지를 호출해야 함

    ⚡️  현재 게시물의 댓글에 마지막 페이지를 알아낸 후, 마지막 페이지를 다시 호출하는 방식으로 동작
          ✓  마지막 페이지의 호출은 total 값과 size 값을 이용해서 마지막 페이지를 계산하고 다시 Axios로 호출하는방식

 

reply.js의 getList()는 마지막 페이지로 호출할 수 있는 goLast 변수를 추가해서 수정

  
async function getList({bno, page, size, goLast}){
const result = await axios.get(`/api/replies/list/${bno}?page=${page}`, {params: {page, size}})
if(goLast) {
const total = result.data.total
const lastPage = parseInt(Math.ceil(total/size))
return getList({bno:bno, page:lastPage, size:size})
}
return result.data
}

 

  📍  read.html에서 처음부터 댓글의 마지막 페이지를 보고 싶다면 printReplies()를 호출할 때 true 값을 추가


  
//printReplies(1, 10); // 무조건 호출
printReplies(1, 10, true);

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts