diff --git a/1 WEEK/.idea/.gitignore b/1 WEEK/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/1 WEEK/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/1 WEEK/.idea/1 WEEK.iml b/1 WEEK/.idea/1 WEEK.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/1 WEEK/.idea/1 WEEK.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/1 WEEK/.idea/misc.xml b/1 WEEK/.idea/misc.xml new file mode 100644 index 00000000..639900d1 --- /dev/null +++ b/1 WEEK/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/1 WEEK/.idea/modules.xml b/1 WEEK/.idea/modules.xml new file mode 100644 index 00000000..ef1d3910 --- /dev/null +++ b/1 WEEK/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/1 WEEK/.idea/vcs.xml b/1 WEEK/.idea/vcs.xml new file mode 100644 index 00000000..6c0b8635 --- /dev/null +++ b/1 WEEK/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/1 WEEK/Week1/.gitignore b/1 WEEK/Week1/.gitignore new file mode 100644 index 00000000..f68d1099 --- /dev/null +++ b/1 WEEK/Week1/.gitignore @@ -0,0 +1,29 @@ +### IntelliJ IDEA ### +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/1 WEEK/Week1/.idea/.gitignore b/1 WEEK/Week1/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/1 WEEK/Week1/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/1 WEEK/Week1/.idea/misc.xml b/1 WEEK/Week1/.idea/misc.xml new file mode 100644 index 00000000..ef89d983 --- /dev/null +++ b/1 WEEK/Week1/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/1 WEEK/Week1/.idea/modules.xml b/1 WEEK/Week1/.idea/modules.xml new file mode 100644 index 00000000..59971b14 --- /dev/null +++ b/1 WEEK/Week1/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/1 WEEK/Week1/.idea/uiDesigner.xml b/1 WEEK/Week1/.idea/uiDesigner.xml new file mode 100644 index 00000000..2b63946d --- /dev/null +++ b/1 WEEK/Week1/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/1 WEEK/Week1/.idea/vcs.xml b/1 WEEK/Week1/.idea/vcs.xml new file mode 100644 index 00000000..b2bdec2d --- /dev/null +++ b/1 WEEK/Week1/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/1 WEEK/Week1/Readme.md b/1 WEEK/Week1/Readme.md new file mode 100644 index 00000000..2977b407 --- /dev/null +++ b/1 WEEK/Week1/Readme.md @@ -0,0 +1,58 @@ +# 회원 가입 및 조회 기능 구현 + +--- + +## package member +### GradeType +- Member 클래스의 grade를 enum타입으로 사용 + +### Member +- member의 id, name, grade 를 getter setter함 + +### Product +- product의 id, name, price, discountedPrice getter setter 함 + + +--- + + +## package repository +### MemberRepository +- save 와 findById 메소드 정의 + +### MemoryMemberRepository +- MemberRepository 인터페이스 구현 +- 동시성 해결을 위해 Map 대신 ConcurrentHashMap 사용 + + private static ConcurrentHashMap store = new ConcurrentHashMap<>(); + +- public Member save() 에서 id 호출 시 1씩 증가하도록 함 +- public Member findById() - memberId를 키 값으로 Member 반환 + +### ProductRepsoitory +- save 메소드 정의 + +### MemoryProductRepository +- ProductRepository 인터페이스 구현 +- 동시성 해결을 위해 Map 대신 ConcurrentHashMap 사용 +- id 호출 시 1씩 증가하도록 함 + + +--- + +## package service +### MemberSerivce +- member의 signUp과 findByName 구현 +- private static final 을 사용해 외부에서의 접근과 재할당을 방지 + + private static final MemoryMemberRepository memoryMemberRepository = new MemoryMemberRepository(); + +### ProductService +- Member의 id 로 해당 member 의 grade를 확인하고, VIP 등급은 30% 할인을 적용시킴 + + private static double vipDiscount = 0.3; //vip할인율 + + +### Main +- 회원가입 후 고유 id 출력과 고유 id를 사용해 Member의 name을 출력하는 기능을 구현 +- 상품이름과 삼품가격을 입력하고 할인된 가격을 출력하도록 함 \ No newline at end of file diff --git a/1 WEEK/Week1/Week1.iml b/1 WEEK/Week1/Week1.iml new file mode 100644 index 00000000..c90834f2 --- /dev/null +++ b/1 WEEK/Week1/Week1.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/1 WEEK/Week1/src/member/GradeType.java b/1 WEEK/Week1/src/member/GradeType.java new file mode 100644 index 00000000..21ecba1f --- /dev/null +++ b/1 WEEK/Week1/src/member/GradeType.java @@ -0,0 +1,5 @@ +package member; + +public enum GradeType { + NORMAL, VIP +} diff --git a/1 WEEK/Week1/src/member/Member.java b/1 WEEK/Week1/src/member/Member.java new file mode 100644 index 00000000..c65b9855 --- /dev/null +++ b/1 WEEK/Week1/src/member/Member.java @@ -0,0 +1,47 @@ +package member; + +public class Member { + + private Long id; + private String name; + private GradeType grade; + + + + + //getter setter + public void Member(String name, GradeType grade) { + this.name = name; + this.grade = grade; + } + + public Member(String name, GradeType grade) { + this.name = name; + this.grade = grade; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public GradeType getGrade() { + return grade; + } + + public void setGrade(GradeType grade) { + this.grade = grade; + } + +} diff --git a/1 WEEK/Week1/src/member/Product.java b/1 WEEK/Week1/src/member/Product.java new file mode 100644 index 00000000..bc94b4aa --- /dev/null +++ b/1 WEEK/Week1/src/member/Product.java @@ -0,0 +1,46 @@ +package member; + +public class Product { + + private Long id; + private String name; + private Long price; + private Double discountedPrice; + + public Product(String name, Long price) { + this.name = name; + this.price = price; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getPrice() { + return price; + } + + public void setPrice(Long price) { + this.price = price; + } + + public Double getDiscountedPrice() { + return discountedPrice; + } + + public void setDiscountedPrice(Double discountedPrice) { + this.discountedPrice = discountedPrice; + } +} diff --git a/1 WEEK/Week1/src/repository/MemberRepository.java b/1 WEEK/Week1/src/repository/MemberRepository.java new file mode 100644 index 00000000..5f66e677 --- /dev/null +++ b/1 WEEK/Week1/src/repository/MemberRepository.java @@ -0,0 +1,10 @@ +package repository; + +import member.Member; + +public interface MemberRepository { + public Member save(Member member); + + public Member findById(Long memberId); + +} diff --git a/1 WEEK/Week1/src/repository/MemoryMemberRepository.java b/1 WEEK/Week1/src/repository/MemoryMemberRepository.java new file mode 100644 index 00000000..d918d788 --- /dev/null +++ b/1 WEEK/Week1/src/repository/MemoryMemberRepository.java @@ -0,0 +1,24 @@ +package repository; + +import member.Member; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class MemoryMemberRepository implements MemberRepository{ + + + private static ConcurrentHashMap store = new ConcurrentHashMap<>(); + private static long incrementID = 0L; + + public Member save(Member member) { + member.setId(++incrementID); //호출 시 1씩 증가 + store.put(member.getId(), member); + return member; + } + + public Member findById(Long memberId) { + return store.get(memberId); + } +} diff --git a/1 WEEK/Week1/src/repository/MemoryProductRepository.java b/1 WEEK/Week1/src/repository/MemoryProductRepository.java new file mode 100644 index 00000000..36050e1c --- /dev/null +++ b/1 WEEK/Week1/src/repository/MemoryProductRepository.java @@ -0,0 +1,28 @@ +package repository; + +import member.GradeType; +import member.Member; +import member.Product; + + +import java.util.concurrent.ConcurrentHashMap; + +public class MemoryProductRepository implements ProductRepository{ + + //memberId, product + private static ConcurrentHashMap store = new ConcurrentHashMap<>(); + private static long incrementId = 0L; + + + + //키 값으로 멤버의 고유 id 저장 + public Product save(Long memberId, Product product) { + product.setId(++incrementId); + + + store.put(memberId, product); + return product; + } + + +} diff --git a/1 WEEK/Week1/src/repository/ProductRepository.java b/1 WEEK/Week1/src/repository/ProductRepository.java new file mode 100644 index 00000000..1821000d --- /dev/null +++ b/1 WEEK/Week1/src/repository/ProductRepository.java @@ -0,0 +1,9 @@ +package repository; + +import member.Product; + +public interface ProductRepository { + public Product save(Long memberId, Product product); + + +} diff --git a/1 WEEK/Week1/src/service/Main.java b/1 WEEK/Week1/src/service/Main.java new file mode 100644 index 00000000..a0033cfe --- /dev/null +++ b/1 WEEK/Week1/src/service/Main.java @@ -0,0 +1,48 @@ +package service; + +import member.GradeType; +import member.Member; +import member.Product; +import repository.MemoryMemberRepository; + +public class Main { + public static void main(String[] args) { + MemberService memberSerive = new MemberService(); + + //회원 name, grade 지정 + Member member1 = new Member("name1", GradeType.VIP); + Member member2 = new Member("name2", GradeType.NORMAL); + + + //회원가입 + Long id1 = memberSerive.signUp(member1); + Long id2 = memberSerive.signUp(member2); + System.out.println(id1); + System.out.println(id2); + + + //id 검색 -> 앞서 지정한 name 출력 + Member findMember1 = memberSerive.findByName(id1); + Member findMember2 = memberSerive.findByName(id2); + System.out.println(findMember1.getName()); + System.out.println(findMember2.getName()); + + + //상품 주문 + //고객의 고유 id, 상품 + ProductService productService = new ProductService(); + Product product1 = new Product("상품1", 10000L);//상품 이름과 가격 + Product product2 = new Product("상품2", 20000L); + productService.order(id1, product1); + productService.order(id1, product1); //key값을 member의 id로, value에 product 주입 + productService.order(id2, product2); + + //등급별 할인된 가격 확인 + System.out.println(product1.getDiscountedPrice()); + System.out.println(product2.getDiscountedPrice()); + + + + + } +} diff --git a/1 WEEK/Week1/src/service/MemberService.java b/1 WEEK/Week1/src/service/MemberService.java new file mode 100644 index 00000000..2daf8688 --- /dev/null +++ b/1 WEEK/Week1/src/service/MemberService.java @@ -0,0 +1,23 @@ +package service; + +import member.Member; +import repository.MemberRepository; +import repository.MemoryMemberRepository; + +public class MemberService { + + + private static final MemoryMemberRepository memoryMemberRepository = new MemoryMemberRepository(); + + + public Long signUp(Member member) { + memoryMemberRepository.save(member); + return member.getId(); + } + + public Member findByName(Long memberId) { + Member name = memoryMemberRepository.findById(memberId); + return name; + } + +} diff --git a/1 WEEK/Week1/src/service/ProductService.java b/1 WEEK/Week1/src/service/ProductService.java new file mode 100644 index 00000000..7efd2201 --- /dev/null +++ b/1 WEEK/Week1/src/service/ProductService.java @@ -0,0 +1,37 @@ +package service; + +import member.GradeType; +import member.Member; +import member.Product; +import repository.MemoryMemberRepository; +import repository.MemoryProductRepository; +import repository.ProductRepository; + +public class ProductService { + + private static double vipDiscount = 0.3; //vip할인율 + + private static final MemoryProductRepository memoryProductRepository = new MemoryProductRepository(); + public Long order(Long memberId, Product product) { + + //store한 member에 접근 + MemoryMemberRepository memoryMemberRepository = new MemoryMemberRepository(); + Member member = memoryMemberRepository.findById(memberId); + + Long price = product.getPrice(); + Double discountedPrice = 0.0; //default + + + if(member.getGrade().equals(GradeType.VIP)) { + discountedPrice = price - price * vipDiscount; + } else { //GradeType.NORMAL + discountedPrice = (double)price; + } + product.setDiscountedPrice(discountedPrice); + + memoryProductRepository.save(memberId, product); + return product.getId(); + } + + +} diff --git a/10 WEEK/studentManagement/.gitignore b/10 WEEK/studentManagement/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/10 WEEK/studentManagement/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/10 WEEK/studentManagement/10WEEK.md b/10 WEEK/studentManagement/10WEEK.md new file mode 100644 index 00000000..b85c7256 --- /dev/null +++ b/10 WEEK/studentManagement/10WEEK.md @@ -0,0 +1,328 @@ +# 10WEEK MISSION + + +*Student class* +```java + private String stuName; + private Long stuNum; + private int stuGrade; + private String stuMajort; //ENUM처리 고려 +``` + +처음 타입을 모두 String으로 하였으나 MAP에서 KEY값을 지정하기 위해 stuNum타입을 Long으로 변경하였다. + +따라서 학번을 기준으로 조회 및 삭제가 가능하다. + + +*StudentRepository* +```java + + Student save(Student stu); + + Student findByStuNum(Long stuNum); + + List findAll(); + + void delteByStuNum(Long stuNum); + + void deleteAll(); + + Student updateStu(Long stuNum, Student stu); + + int countStu(); +``` + +`CRUD`를 작성 중 어려웠던 부분은 각 `메서드명`을 어떻게 지정하는지 결정하는 것이다.
+보편적으로 많이 사용하는 `JPA`의 `CRUD` `메서드명`을 참고하여 결정했다. + + +*StudentServiceTest* +```java + @BeforeEach + public void BeforeEach() { + Student stu1 = new Student(); + stu1.setStuNum(2100000L); + stu1.setStuName("PARK"); + stu1.setStuGrade(2); + stu1.setStuMajort("SoftwareDept"); + studentService.signUp(stu1); + + Student stu2 = new Student(); + stu2.setStuNum(1900000L); + stu2.setStuName("KIM"); + stu2.setStuGrade(4); + stu2.setStuMajort("SoftwareDept"); + studentService.signUp(stu2); + } + + @AfterEach + public void afterEach() { + studentService.deleteAllStudent(); + } +``` + +`@BeforeEach`를 활용하여 각 단위 테스트에서 필요한 `save` 중복 코드를 방지했다. +또한, `@AfterEach`를 이용하여 각 단위 테스트가 종료될 때 초기화를 수행함으로써 +통합 테스트를 효과적으로 수행할 수 있게 구현하였다. + +## Builder Pattern + +빌더 패턴 장점 +- 필요한 데이터만 설정 가능 +- 유연성 확보 가능 +- 가독성이 높아짐 +- 불변성 확보 가능 + +*Student* +``` +@Builder +public class Student +``` + +*StudentServiceTest* +```java + public void BeforeEach() { + Student stu1 = Student.builder() + .stuNum(2100000L) + .stuName("PARK") + .stuGrade(2) + .stuMajor("SoftwareDept") + .build(); + studentService.signUp(stu1); + + + Student stu2 = Student.builder() + .stuNum(1900000L) + .stuName("KIM") + .stuGrade(4) + .stuMajor("SoftwareDept") + .build(); + studentService.signUp(stu2); + } +``` + +이전 코드보다 훨씬 직관적이다. + + + + + + + +## Controller + +### REST API + +| METHOD | 정의 | CRUD | +|--------|-----------------------|--------------------| +| GET | 리소스를 조회하고 정보를 가져온다 | READ | +| POST | 리소스를 생성한다 | CREATE | +| PUT | 해당 리소스를 수정한다. | CREATE OR UPDATE | +| PATCH | 해당 리소스를 수정한다.(일부만 수정) | UPDATE | +| DELETE | 해당 리소스를 삭제합니다. | DELETE | + + +*초기 데이터 - StudentManagementApplication* +``` + @PostConstruct + public void post() { + Student stu1 = Student.builder() + .stuNum(2100000L) + .stuName("PARK") + .stuGrade(2) + .stuMajor("SoftwareDept") + .build(); + studentService.signUp(stu1); + + Student stu2 = Student.builder() + .stuNum(1900000L) + .stuName("KIM") + .stuGrade(4) + .stuMajor("SoftwareDept") + .build(); + studentService.signUp(stu2); + } +``` +```json + { + "stuName": "PARK", + "stuNum": 2100000, + "stuGrade": 2, + "stuMajor": "SoftwareDept" + }, + { + "stuName": "KIM", + "stuNum": 1900000, + "stuGrade": 4, + "stuMajor": "SoftwareDept" + } +``` + + +## Postman Test + +### 학생 등록 - POST http://localhost:8080/students + +```java + @PostMapping("/students") //학생 등록 + public List signUp(@RequestBody Student student) { + studentService.signUp(student); + return studentService.showAllStudent(); //학생 등록 확인을 위해 전체 출력 + } +``` +입력데이터 +```json + { + "stuName": "TEST", + "stuNum": 1800000, + "stuGrade": 1, + "stuMajor": "ComputerDept" + } +``` + +*확인* + +![스크린샷 2024-01-24 152533.png](img%2F%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202024-01-24%20152533.png) + +- 초기데이터와 입력된 데이터 확인 가능 + + +### 학생 단일 조회 - GET http://localhost:8080/students/2100000 + +```java + @GetMapping("/students/{stuNum}") // 단일 학생 조회 + public Student showStudent(@PathVariable Long stuNum) { + return studentService.showStudent(stuNum); + } +``` + +![단일조회.png](img%2F%EB%8B%A8%EC%9D%BC%EC%A1%B0%ED%9A%8C.png) + +- 학번 2100000의 데이터 정보 확인 가능 + + + +### 모든 학생 조회 - GET http://localhost:8080/students + +```java + @GetMapping("/students") // 모든 학생 조회 + public List showAllStudent() { + return studentService.showAllStudent(); + } +``` + +![모든 학생 조회.png](img%2F%EB%AA%A8%EB%93%A0%20%ED%95%99%EC%83%9D%20%EC%A1%B0%ED%9A%8C.png) + +- 초기 학생 데이터값 전체 확인 가능 + + + +### 단일 학생 삭제 - DELETE http://localhost:8080/students/2100000 + +```java + @DeleteMapping("/students") // 다중 학생 삭제 + public List deleteAllStudent() { + studentService.deleteAllStudent(); + return studentService.showAllStudent(); + } +``` + +![단일삭제.png](img%2F%EB%8B%A8%EC%9D%BC%EC%82%AD%EC%A0%9C.png) + +- 학번 2100000 데이터가 제거됨 + +### 다중 학생 삭제 - DELETE http://localhost:8080/students + +```java + @DeleteMapping("/students") // 다중 학생 삭제 + public List deleteAllStudent() { + studentService.deleteAllStudent(); + return studentService.showAllStudent(); + } +``` + +![다중 삭제.png](img%2F%EB%8B%A4%EC%A4%91%20%EC%82%AD%EC%A0%9C.png) + +- 전체 학생 데이터가 제거됨 + +### 단일 학생 정보 수정 - Patch http://localhost:8080/students/2100000 + +```java + @PatchMapping("/students/{stuNum}") // 단일 학생 정보 수정 + public Student updateStudent(@PathVariable Long stuNum, @RequestBody Student stu) { + studentService.updateStudent(stuNum, stu); + return studentService.showStudent(stuNum); + } +``` + +*전송 데이터* +```json + { + "stuName": "LEE", + "stuGrade": 4 + } +``` + +*결과* +```json +{ + "stuName": "LEE", + "stuNum": null, + "stuGrade": 4, + "stuMajor": null +} +``` + +- 지정하지 않은 데이터에서 `null`이 되었다. + +`StudentRepository 수정` +```java + public Student updateStu(Long stuNum, Student stu) { + Student existData = store.get(stuNum); + if(stu.getStuNum() == null) + stu.setStuNum(existData.getStuNum()); + if (stu.getStuName() == null) + stu.setStuName(existData.getStuName()); + + Integer stuGrade = stu.getStuGrade(); + if (stuGrade == null) + stu.setStuGrade(existData.getStuGrade()); + + if (stu.getStuMajor() == null) + stu.setStuMajor(existData.getStuMajor()); + + + return store.replace(stuNum, stu); + } +``` + +- `null`이 들어온 데이터에 대해서는 기존 데이터의 값을 새로운 데이터에 참조하게 수정했다. +- `stuGrade` 같은 경우 `int` 형이기에 `null`을 처리하지 못한다. 따라서 `Integer`로 형변환하였다. + +![단일 학생 수정.png](img%2F%EB%8B%A8%EC%9D%BC%20%ED%95%99%EC%83%9D%20%EC%88%98%EC%A0%95.png) + +- 학번 2100000의 데이터의 Name과 Grade만 변경된것을 확인할 수 있다. + + + +### 총 학생 수 조회 - GET http://localhost:8080/students/count + +```java + @GetMapping("/students/count") // 총 학생 조회 + public int countStudent() { + return studentService.countAllStudent(); + } +``` + +![총 학생 수.png](img%2F%EC%B4%9D%20%ED%95%99%EC%83%9D%20%EC%88%98.png) + + + + + + + + + + + diff --git a/10 WEEK/studentManagement/build.gradle b/10 WEEK/studentManagement/build.gradle new file mode 100644 index 00000000..b6ab7603 --- /dev/null +++ b/10 WEEK/studentManagement/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.2' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'fx' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/10 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.jar b/10 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/10 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.jar differ diff --git a/10 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.properties b/10 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1af9e093 --- /dev/null +++ b/10 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/10 WEEK/studentManagement/gradlew b/10 WEEK/studentManagement/gradlew new file mode 100644 index 00000000..1aa94a42 --- /dev/null +++ b/10 WEEK/studentManagement/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/10 WEEK/studentManagement/gradlew.bat b/10 WEEK/studentManagement/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/10 WEEK/studentManagement/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git "a/10 WEEK/studentManagement/img/\353\213\244\354\244\221 \354\202\255\354\240\234.png" "b/10 WEEK/studentManagement/img/\353\213\244\354\244\221 \354\202\255\354\240\234.png" new file mode 100644 index 00000000..c415009c Binary files /dev/null and "b/10 WEEK/studentManagement/img/\353\213\244\354\244\221 \354\202\255\354\240\234.png" differ diff --git "a/10 WEEK/studentManagement/img/\353\213\250\354\235\274 \355\225\231\354\203\235 \354\210\230\354\240\225.png" "b/10 WEEK/studentManagement/img/\353\213\250\354\235\274 \355\225\231\354\203\235 \354\210\230\354\240\225.png" new file mode 100644 index 00000000..3cf49397 Binary files /dev/null and "b/10 WEEK/studentManagement/img/\353\213\250\354\235\274 \355\225\231\354\203\235 \354\210\230\354\240\225.png" differ diff --git "a/10 WEEK/studentManagement/img/\353\213\250\354\235\274\354\202\255\354\240\234.png" "b/10 WEEK/studentManagement/img/\353\213\250\354\235\274\354\202\255\354\240\234.png" new file mode 100644 index 00000000..0ea1cd6b Binary files /dev/null and "b/10 WEEK/studentManagement/img/\353\213\250\354\235\274\354\202\255\354\240\234.png" differ diff --git "a/10 WEEK/studentManagement/img/\353\213\250\354\235\274\354\241\260\355\232\214.png" "b/10 WEEK/studentManagement/img/\353\213\250\354\235\274\354\241\260\355\232\214.png" new file mode 100644 index 00000000..31af01a6 Binary files /dev/null and "b/10 WEEK/studentManagement/img/\353\213\250\354\235\274\354\241\260\355\232\214.png" differ diff --git "a/10 WEEK/studentManagement/img/\353\252\250\353\223\240 \355\225\231\354\203\235 \354\241\260\355\232\214.png" "b/10 WEEK/studentManagement/img/\353\252\250\353\223\240 \355\225\231\354\203\235 \354\241\260\355\232\214.png" new file mode 100644 index 00000000..2ca641f3 Binary files /dev/null and "b/10 WEEK/studentManagement/img/\353\252\250\353\223\240 \355\225\231\354\203\235 \354\241\260\355\232\214.png" differ diff --git "a/10 WEEK/studentManagement/img/\354\212\244\355\201\254\353\246\260\354\203\267 2024-01-24 152533.png" "b/10 WEEK/studentManagement/img/\354\212\244\355\201\254\353\246\260\354\203\267 2024-01-24 152533.png" new file mode 100644 index 00000000..b2d0a4e4 Binary files /dev/null and "b/10 WEEK/studentManagement/img/\354\212\244\355\201\254\353\246\260\354\203\267 2024-01-24 152533.png" differ diff --git "a/10 WEEK/studentManagement/img/\354\264\235 \355\225\231\354\203\235 \354\210\230.png" "b/10 WEEK/studentManagement/img/\354\264\235 \355\225\231\354\203\235 \354\210\230.png" new file mode 100644 index 00000000..62e1559c Binary files /dev/null and "b/10 WEEK/studentManagement/img/\354\264\235 \355\225\231\354\203\235 \354\210\230.png" differ diff --git a/10 WEEK/studentManagement/settings.gradle b/10 WEEK/studentManagement/settings.gradle new file mode 100644 index 00000000..40561028 --- /dev/null +++ b/10 WEEK/studentManagement/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'studentManagement' diff --git a/10 WEEK/studentManagement/src/main/java/fx/studentManagement/StudentManagementApplication.java b/10 WEEK/studentManagement/src/main/java/fx/studentManagement/StudentManagementApplication.java new file mode 100644 index 00000000..cbe3d012 --- /dev/null +++ b/10 WEEK/studentManagement/src/main/java/fx/studentManagement/StudentManagementApplication.java @@ -0,0 +1,13 @@ +package fx.studentManagement; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class StudentManagementApplication { + + public static void main(String[] args) { + SpringApplication.run(StudentManagementApplication.class, args); + } + +} diff --git a/10 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/StudentController.java b/10 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/StudentController.java new file mode 100644 index 00000000..e7caae79 --- /dev/null +++ b/10 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/StudentController.java @@ -0,0 +1,99 @@ +package fx.studentManagement.controller; + +import fx.studentManagement.entity.Student; +import fx.studentManagement.service.StudentService; +import jakarta.annotation.PostConstruct; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +@RestController +@AllArgsConstructor +@RequestMapping("/students") +public class StudentController { + + private final StudentService studentService; + + + @PostMapping //학생 등록 + public ResponseEntity signUp(@RequestBody Student student) { + try { + studentService.signUp(student); + return new ResponseEntity<>("학생 등록 성공", HttpStatus.OK); // 학생 등록 확인을 위해 전체 출력 + } catch (Exception e) { + return new ResponseEntity<>("이미 존재하는 학생입니다.", HttpStatus.BAD_REQUEST); + } + } + + @GetMapping("/{studentNumber}") // 단일 학생 조회 + public ResponseEntity showStudent(@PathVariable Long studentNumber) { + try { + return new ResponseEntity<>(studentService.showStudent(studentNumber), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>("존재하지 않는 학생입니다.", HttpStatus.BAD_REQUEST); + } + } + + @GetMapping // 모든 학생 조회 + public ResponseEntity showAllStudent() { + try { + return new ResponseEntity<>(studentService.showAllStudent(), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>("존재하는 학생이 없습니다.", HttpStatus.BAD_REQUEST); + } + } + + @DeleteMapping("/{studentNumber}") // 단일 학생 삭제 + public ResponseEntity deleteStudent(@PathVariable Long studentNumber) { + try { + studentService.deleteStudent(studentNumber); + return new ResponseEntity<>("학번 : " + studentNumber + " 정보 삭제 성공", HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>("존재하지 않는 학생입니다.", HttpStatus.BAD_REQUEST); + } + } + + @DeleteMapping // 다중 학생 삭제 + public ResponseEntity deleteAllStudent() { + studentService.deleteAllStudent(); + return new ResponseEntity<>("학생 전체 정보가 삭제되었습니다.", HttpStatus.OK); + } + + @PatchMapping("/{studentNumber}") // 단일 학생 정보 수정 + public ResponseEntity updateStudent(@PathVariable Long studentNumber, @RequestBody Student student) { + try{ + Student updateStudent = studentService.updateStudent(studentNumber, student); + return new ResponseEntity<>(updateStudent, HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>("학생 정보 수정에 실패했습니다.", HttpStatus.BAD_REQUEST); + } + + } + + @GetMapping("/count") // 총 학생 조회 + public int countStudent() { + return studentService.countAllStudent(); + } + + @PostConstruct + public void post() { + Student stu1 = Student.builder() + .studentNumber(2100000L) + .studentName("PARK") + .studentGrade(2) + .studentMajor("SoftwareDept") + .build(); + studentService.signUp(stu1); + + + Student stu2 = Student.builder() + .studentNumber(1900000L) + .studentName("KIM") + .studentGrade(4) + .studentMajor("SoftwareDept") + .build(); + studentService.signUp(stu2); + } +} diff --git a/10 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/Student.java b/10 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/Student.java new file mode 100644 index 00000000..e6b57b9b --- /dev/null +++ b/10 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/Student.java @@ -0,0 +1,21 @@ +package fx.studentManagement.entity; + +/* +* 이름, 학번, 학년, 전공 +*/ + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@Builder +public class Student { + + private String studentName; + private Long studentNumber; + private Integer studentGrade; + private String studentMajor; //ENUM처리 고려 + +} diff --git a/10 WEEK/studentManagement/src/main/java/fx/studentManagement/repository/StudentRepository.java b/10 WEEK/studentManagement/src/main/java/fx/studentManagement/repository/StudentRepository.java new file mode 100644 index 00000000..140a4af7 --- /dev/null +++ b/10 WEEK/studentManagement/src/main/java/fx/studentManagement/repository/StudentRepository.java @@ -0,0 +1,44 @@ +package fx.studentManagement.repository; + +import fx.studentManagement.entity.Student; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Repository +public class StudentRepository { + + private static Map store = new ConcurrentHashMap<>(); + + public Student save(Student student) { + store.put(student.getStudentNumber(), student); + return student; + } + + public Student findByStudentNumber(Long studentNumber) { + return store.get(studentNumber); + } + + public List findAll() { + return new ArrayList<>(store.values()); + } + + public void delteByStudentNumber(Long studentNumber) { + store.remove(studentNumber); + } + + public void deleteAll() { + store.clear(); + } + + public Student updateStudent(Long studentNumber, Student student) { + return store.replace(studentNumber, student); + } + + public int countStudent() { + return store.size(); + } +} diff --git a/10 WEEK/studentManagement/src/main/java/fx/studentManagement/service/StudentService.java b/10 WEEK/studentManagement/src/main/java/fx/studentManagement/service/StudentService.java new file mode 100644 index 00000000..1c1bc890 --- /dev/null +++ b/10 WEEK/studentManagement/src/main/java/fx/studentManagement/service/StudentService.java @@ -0,0 +1,57 @@ +package fx.studentManagement.service; + +import fx.studentManagement.entity.Student; +import fx.studentManagement.repository.StudentRepository; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@AllArgsConstructor +@Slf4j +public class StudentService { + + private final StudentRepository studentRepository; + + public void signUp(Student student) { //학생 입력 + + if(studentRepository.findByStudentNumber(student.getStudentNumber()) != null) + throw new RuntimeException("이미 존재하는 학생입니다."); + + studentRepository.save(student); + } + + public Student showStudent(Long studentNumber) { //단일 학생 조회 + if (studentRepository.findByStudentNumber(studentNumber) == null) + throw new RuntimeException("존재하지 않는 학생입니다."); + return studentRepository.findByStudentNumber(studentNumber); + } + + public List showAllStudent() { //모든 학생 조회 + if(studentRepository.findAll() == null) + throw new RuntimeException("존재하는 학생이 없습니다."); + return studentRepository.findAll(); + } + + public void deleteStudent(Long studentNumber) { // 단일 학생 삭제 + if(studentRepository.findByStudentNumber(studentNumber) == null) + throw new RuntimeException("존재하지 않는 학생입니다."); + studentRepository.delteByStudentNumber(studentNumber); + } + + public void deleteAllStudent() { // 다중 학생 삭제 + studentRepository.deleteAll(); + } + + public Student updateStudent(Long studentNumber, Student student) { //단일 학생 정보 수정 + if(studentRepository.findByStudentNumber(studentNumber) == null) + throw new RuntimeException("존재하지 않는 학생입니다."); + return studentRepository.updateStudent(studentNumber, student); + } + + public int countAllStudent() { + return studentRepository.countStudent(); + } +} \ No newline at end of file diff --git a/10 WEEK/studentManagement/src/main/resources/application.properties b/10 WEEK/studentManagement/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/10 WEEK/studentManagement/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/10 WEEK/studentManagement/src/test/java/fx/studentManagement/StudentManagementApplicationTests.java b/10 WEEK/studentManagement/src/test/java/fx/studentManagement/StudentManagementApplicationTests.java new file mode 100644 index 00000000..e4d2e483 --- /dev/null +++ b/10 WEEK/studentManagement/src/test/java/fx/studentManagement/StudentManagementApplicationTests.java @@ -0,0 +1,13 @@ +package fx.studentManagement; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class StudentManagementApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/10 WEEK/studentManagement/src/test/java/fx/studentManagement/service/StudentServiceTest.java b/10 WEEK/studentManagement/src/test/java/fx/studentManagement/service/StudentServiceTest.java new file mode 100644 index 00000000..52149fa4 --- /dev/null +++ b/10 WEEK/studentManagement/src/test/java/fx/studentManagement/service/StudentServiceTest.java @@ -0,0 +1,150 @@ +package fx.studentManagement.service; + +import fx.studentManagement.entity.Student; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + + +@SpringBootTest +class StudentServiceTest { + + @Autowired + private StudentService studentService; + + @BeforeEach + public void BeforeEach() { + Student stu1 = Student.builder() + .studentNumber(21000L) + .studentName("PARK") + .studentGrade(2) + .studentMajor("SoftwareDept") + .build(); + studentService.signUp(stu1); + + + Student stu2 = Student.builder() + .studentNumber(19000L) + .studentName("KIM") + .studentGrade(4) + .studentMajor("SoftwareDept") + .build(); + studentService.signUp(stu2); + } + + @AfterEach + public void afterEach() { + studentService.deleteAllStudent(); + } + + @Test + void signUp() { + //give + Student stu = Student.builder() + .studentNumber(20000L) + .studentName("LEE") + .studentGrade(3) + .studentMajor("SoftwareDept") + .build(); + + + //when + studentService.signUp(stu); + + //then + Assertions.assertThat(stu.getStudentNumber()).isEqualTo(20000L); + Assertions.assertThat(stu.getStudentName()).isEqualTo("LEE"); + Assertions.assertThat(stu.getStudentGrade()).isEqualTo(3); + Assertions.assertThat(stu.getStudentMajor()).isEqualTo("SoftwareDept"); + } + + @Test + void showStudent() { + //give + + //when + Student stu1 = studentService.showStudent(21000L); + Student stu2 = studentService.showStudent(19000L); + + //then + Assertions.assertThat(stu1.getStudentNumber()).isEqualTo(21000L); + Assertions.assertThat(stu1.getStudentName()).isEqualTo("PARK"); + Assertions.assertThat(stu1.getStudentGrade()).isEqualTo(2); + Assertions.assertThat(stu1.getStudentMajor()).isEqualTo("SoftwareDept"); + + Assertions.assertThat(stu2.getStudentNumber()).isEqualTo(19000L); + Assertions.assertThat(stu2.getStudentName()).isEqualTo("KIM"); + Assertions.assertThat(stu2.getStudentGrade()).isEqualTo(4); + Assertions.assertThat(stu2.getStudentMajor()).isEqualTo("SoftwareDept"); + } + + @Test + void showAllStudent() { + //give + + //when + List students = studentService.showAllStudent(); + //then + Assertions.assertThat(students.size()).isEqualTo(2); + } + + @Test + void deleteStudent() { + //give + + //when + studentService.deleteStudent(21000L); + List students = studentService.showAllStudent(); + System.out.println("students = " + students); + //then + Assertions.assertThat(students.size()).isEqualTo(3); + + } + + @Test + void deleteAllStudent() { + //give + + //when + studentService.deleteAllStudent(); + List students = studentService.showAllStudent(); + + //then + Assertions.assertThat(students.size()).isEqualTo(0); + + } + + @Test + void updateStudent() { + //give + Student updateStu = Student.builder() + .studentName("PARK") + .studentGrade(1) + .studentMajor("ComputerDept") + .build(); + + //when + studentService.updateStudent(21000L, updateStu); + Student student = studentService.showStudent(21000L); + //then + Assertions.assertThat(student.getStudentName()).isEqualTo("PARK"); + Assertions.assertThat(student.getStudentGrade()).isEqualTo(1); + Assertions.assertThat(student.getStudentMajor()).isEqualTo("ComputerDept"); + + } + + @Test + void countAllStudent() { + //give + + //when + int cnt = studentService.countAllStudent(); + //then + Assertions.assertThat(cnt).isEqualTo(2); + } +} \ No newline at end of file diff --git a/12 WEEK/studentManagement/.gitignore b/12 WEEK/studentManagement/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/12 WEEK/studentManagement/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/12 WEEK/studentManagement/12WEEK.md b/12 WEEK/studentManagement/12WEEK.md new file mode 100644 index 00000000..8a343159 --- /dev/null +++ b/12 WEEK/studentManagement/12WEEK.md @@ -0,0 +1,369 @@ +# 12 WEEK + + +## Form 생성 +- 기존 `Student` 로 통일했던 폼을 `EditStudentForm`과 `SignUpForm`으로 분리 작업 진행 + +*SignUpForm* +```java +public class SignUpForm { +//학번, 이름, 생년월일, 학과, 학기, 주소, 학년 + private Long studentNumber; + private String studentName; + private String studentBirth; + private Major studentMajor; + private int studentSemester; + private String studentAddress; + private int studentGrade; +} +``` + +*EditStudentForm* +```java +public class EditStudentForm { + private String StudentName; + private String StudentAddress; +} +``` + +*Student 필드 추가* +```java +public class Student { +//학번, 학년, 이름, 생년,생월,생일,학과, 학기,주소 + private Long studentNumber; //학번 + private int studentGrade; + private String studentName; + private int studentBirthYear; + private int studentBirthMonth; + private int studentBirthDay; + private Major studentMajor; + private int studentSemester; + private String studentAddress; +} +``` + +## ENUM +- 학과 명칭은 ENUM으로 처리했다. +```java +public enum Major { + SoftwareDept("소프트웨어학과"), + ComputerEngineeringDept("컴퓨터공학과"), + IndustrialDesignDept("산업디자인학과"); + private String dept; + Major(String dept) {this.dept = dept;} + public String getDept() {return dept;} +} +``` +위와 같은 방식을 사용했지만, 한글로 요청이 들어오는 상황에서 SoftwareDept같이 영어로 지정해놓으면 +자바에서 한글을 다시 영문으로 바꿔야하는 복잡한 문제가 있어 아래와 같이 변경하였다. + +```java +public enum Major { + 소프트웨어학과, + 컴퓨터공학과, + 산업디자인학과 +} +``` + + +## 예외처리 + +*ResponseMessage* +```java +public enum ResponseMessage { + NOT_FOUND_STUDENT("학생을 조회할 수 없습니다."), + ALREADY_EXIST_STUDENT("이미 존재하는 학생입니다."), + SUCCESS_REGISTERED_STUDENT("학생 등록 성공"), + SUCCESS_READ_STUDENT("학생 정보 조회 성공"), + SUCCESS_EDIT_STUDENT_INFO("학생 정보 수정 성공"), + SUCCESS_DELETE_ALL_STUDENT("학생 정보 전체 삭제 성공"), + SUCCESS_DELETE_STUDENT("학생 단일 정보 삭제 성공"); + private final String message; + ResponseMessage(String message) {this.message = message;} + public String getMessage() {return message;} +} +``` +- 각종 예외 메시지를 담는다. + +*DuplicateStudentException* +```java +public class DuplicateStudentException extends RuntimeException { + public DuplicateStudentException() {} + public DuplicateStudentException(String message) {super(message);} + public DuplicateStudentException(String message, Throwable cause) {super(message, cause);} + public DuplicateStudentException(Throwable cause) {super(cause);} + public DuplicateStudentException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);} +} + +``` + +*NotFoundStudentException* + +```java +public class NotFoundStudentException extends RuntimeException { + public NotFoundStudentException() {} + public NotFoundStudentException(String message) {super(message);} + public NotFoundStudentException(String message, Throwable cause) {super(message, cause);} + public NotFoundStudentException(Throwable cause) {super(cause);} + public NotFoundStudentException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);} +} +``` + +*ExceptionAdvice* +```java +@Slf4j +@RestControllerAdvice +public class ExceptionAdvice { + @ExceptionHandler(DuplicateStudentException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity duplicateStudentException(DuplicateStudentException e) { + log.info("이미 존재하는 회원입니다."); + return new ResponseEntity<>(ResponseMessage.ALREADY_EXIST_STUDENT.getMessage(), HttpStatus.BAD_REQUEST); + } + @ExceptionHandler(NotFoundStudentException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity notFoundStudentException(NotFoundStudentException e) { + log.info("학생을 조회할 수 없습니다."); + return new ResponseEntity<>(ResponseMessage.NOT_FOUND_STUDENT.getMessage(), HttpStatus.NOT_FOUND); + } +} +``` +- API 호출하며 생기는 에러에 대한 공통 처리를 해주기 위해 `ExceptionAadvice`를 사용했다. + + +## 유효성검증 + +- Validation +``` +1. 학번 +- 7자리, 숫자 ( 영문 , 특수 기호 금지 ) +2. 이름 +- 2자리 ~ 7자리 , 한글 ( 영문, 특수기호, 숫자 금지) +3. 생년월일 +- format : YYYY_mm_dd +4. 학과 +- 소프트웨어, 컴퓨터공학, 산업디자인학과 외 금지 +5. 학기 +- 1학기 , 2학기 외 금지 +6. 주소 + - null 허용 +7. 학년 +- 1 학년 ~ 5학년 외 금지 +``` + +요구사항을 충족시키기 위해 다음과 같이 gradle 추가 + + + +*build.gradle* +``` + implementation 'org.springframework.boot:spring-boot-starter-validation' +``` + +*SignUpForm 유효성검증* + +```java +public class SignUpForm { +//학번, 이름, 생년월일, 학과, 학기, 주소, 학년 + //https://coderanch.com/t/586580/java/validate-length-number +// @Range(min=7, max=7, message = "- 7자리, 숫자 ( 영문 , 특수 기호 금지 )") + private Long studentNumber; + + @Size(min =2, max = 7) + @Pattern(regexp = "^[가-힣]*$", message = "- 2자리 ~ 7자리 , 한글 ( 영문, 특수기호, 숫자 금지)") + private String studentName; + + @Pattern(regexp = "\\d{4}_\\d{2}_\\d{2}", message = "- format : YYYY_mm_dd") + private String studentBirth; + + @ValidEnum(enumClass = Major.class, message = "- 소프트웨어, 컴퓨터공학, 산업디자인학과 외 금지") + private Major studentMajor; + + @Digits(integer = 1, fraction = 0) + @Min(value = 1) @Max(value = 2, message = "- 1학기 , 2학기 외 금지") + private int studentSemester; + + // - null 허용 + private String studentAddress; + + @Digits(integer = 1, fraction = 0) + @Min(value = 1) @Max(value = 5,message = "- 1 학년 ~ 5학년 외 금지") + private int studentGrade; +} +``` +- `Long Type`에 대한 유효성 검증을 어떻게 처리해야하는지 아직까지 모르겠다....... + +*EditStudentForm* +```java +public class EditStudentForm { + @Size(min =2, max = 7) + @Pattern(regexp = "^[가-힣]*$", message = "- 2자리 ~ 7자리 , 한글 ( 영문, 특수기호, 숫자 금지)") + private String StudentName; + + // - null 허용 + private String StudentAddress; +} + +``` + +### ENUM 유효성 검증을 위한 EnumValidator + +- `https://jsy1110.github.io/2022/enum-class-validation/` 참고 + + +*EnumValidator* +```java +public class EnumValidator implements ConstraintValidator { + private ValidEnum annotation; + @Override + public void initialize(ValidEnum constraintAnnotation) { + this.annotation = constraintAnnotation; + } + @Override + public boolean isValid(Enum value, ConstraintValidatorContext context) { + boolean result = false; + Object[] enumValues = this.annotation.enumClass().getEnumConstants(); + if (enumValues != null) { + for (Object enumValue : enumValues) { + if (value == enumValue) { + result = true; + break; + } + } + } + return result; + } +} +``` + +*ValidEnum* +```java +@Constraint(validatedBy = EnumValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidEnum { + String message() default "Invalid value. This is not permitted."; + Class[] groups() default {}; + Class[] payload() default {}; + Class> enumClass(); +} +``` + +*적용* +```java + @ValidEnum(enumClass = Major.class, message = "- 소프트웨어, 컴퓨터공학, 산업디자인학과 외 금지") + private Major studentMajor; +``` + + + +## SignUp 학생등록 Refactoring + +```java + @PostMapping //학생 등록 + public ResponseEntity signUp(@Valid @RequestBody SignUpForm signUpForm, BindingResult bindingResult) { + if(bindingResult.hasErrors()) + return new ResponseEntity<>(bindingResult.getAllErrors(), HttpStatus.BAD_REQUEST); + studentService.signUp(signUpForm); + return new ResponseEntity<>(ResponseMessage.SUCCESS_REGISTERED_STUDENT.getMessage(), HttpStatus.OK); // 학생 등록 확인을 위해 전체 출력 + } +``` +- 유효성 검증 오류가 발생되면 `ResponseEntity`로 오류 내역을 같이 응답으로 보낸다. + +```java + public void signUp(SignUpForm signUpForm) { //학생 입력 + + if(studentRepository.findByStudentNumber(signUpForm.getStudentNumber()) != null) + throw new DuplicateStudentException(); + + Student student = saveStudent(signUpForm); + + studentRepository.save(student); + } + +``` +- 학생이 존재하지 않으면 `DuplicateStudentException()`을 호출하여 오루메시지가 `ResponseEntity`로 출력된다. + + +```java + private static Student saveStudent(SignUpForm signUpForm) { + String[] studentBirth = signUpForm.getStudentBirth().split("_"); + Student student = Student.builder() + .studentNumber(signUpForm.getStudentNumber()) + .studentGrade(signUpForm.getStudentGrade()) + .studentName(signUpForm.getStudentName()) + .studentBirthYear(Integer.parseInt(studentBirth[0])) + .studentBirthMonth(Integer.parseInt(studentBirth[1])) + .studentBirthDay(Integer.parseInt(studentBirth[2])) + .studentMajor(signUpForm.getStudentMajor()) + .studentSemester(signUpForm.getStudentSemester()) + .studentAddress(signUpForm.getStudentAddress()) + .build(); + return student; + } +``` +- 실제로 DB에 저장되는 `builder`는 메서드를 따로 작성하였다. + + +## editStudentInformation 학생 정보 수정 Refactoring +```java + @PatchMapping("/{studentNumber}") // 단일 학생 정보 수정 + public ResponseEntity editStudentInformation(@PathVariable Long studentNumber,@Valid @RequestBody EditStudentForm editStudentForm, BindingResult bindingResult) { + if(bindingResult.hasErrors()) + return new ResponseEntity<>(bindingResult.getAllErrors(), HttpStatus.BAD_REQUEST); + Student updateStudent = studentService.editStudentInformation(studentNumber, editStudentForm); + return new ResponseEntity<>(updateStudent, HttpStatus.OK); + } +``` + +```java + public Student editStudentInformation(Long studentNumber, EditStudentForm editStudentForm) { //단일 학생 정보 수정 + Student existsStudent = studentRepository.findByStudentNumber(studentNumber); + if(existsStudent == null) + throw new NotFoundStudentException(); + + Student student = editStudentInfo(editStudentForm, existsStudent); + + return studentRepository.updateStudent(studentNumber, student); + } +``` +학생이 존재하지 않으면 `NotFoundStudentException`을 호출하고, `ResponseEntity`로 응답한다. + + +```java + private static Student editStudentInfo(EditStudentForm editStudentForm, Student existsStudent) { + Student student = Student.builder() + .studentNumber(existsStudent.getStudentNumber()) + .studentGrade(existsStudent.getStudentGrade()) + .studentName(editStudentForm.getStudentName()) + .studentBirthYear(existsStudent.getStudentBirthYear()) + .studentBirthMonth(existsStudent.getStudentBirthMonth()) + .studentBirthDay(existsStudent.getStudentBirthDay()) + .studentMajor(existsStudent.getStudentMajor()) + .studentSemester(existsStudent.getStudentSemester()) + .studentAddress(editStudentForm.getStudentAddress()) + .build(); + return student; + } +``` +- `editStudentForm`으로 학생의 정보(이름, 주소)를 받고 기존의 데이터에 덮어 씌우기 위해서 기존 학생 데이터에 `Form`으로 받은 +학생 정보를 참조하였다. + + + +## POSTMAN +학생등록 + +![1등록.png](img%2F1%EB%93%B1%EB%A1%9D.png) + +학생등록 유효성 검증 + +![1등록실패.png](img%2F1%EB%93%B1%EB%A1%9D%EC%8B%A4%ED%8C%A8.png) + +학생 정보 수정 + +![1수정.png](img%2F1%EC%88%98%EC%A0%95.png) + +학생 정보 수정 유효성 검증 + +![1수정실패.png](img%2F1%EC%88%98%EC%A0%95%EC%8B%A4%ED%8C%A8.png) diff --git a/12 WEEK/studentManagement/build.gradle b/12 WEEK/studentManagement/build.gradle new file mode 100644 index 00000000..96b71fcb --- /dev/null +++ b/12 WEEK/studentManagement/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.2' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'fx' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + implementation 'org.springframework.boot:spring-boot-starter-validation' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/12 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.jar b/12 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/12 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.jar differ diff --git a/12 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.properties b/12 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1af9e093 --- /dev/null +++ b/12 WEEK/studentManagement/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/12 WEEK/studentManagement/gradlew b/12 WEEK/studentManagement/gradlew new file mode 100644 index 00000000..1aa94a42 --- /dev/null +++ b/12 WEEK/studentManagement/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/12 WEEK/studentManagement/gradlew.bat b/12 WEEK/studentManagement/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/12 WEEK/studentManagement/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git "a/12 WEEK/studentManagement/img/1\353\223\261\353\241\235.png" "b/12 WEEK/studentManagement/img/1\353\223\261\353\241\235.png" new file mode 100644 index 00000000..568db964 Binary files /dev/null and "b/12 WEEK/studentManagement/img/1\353\223\261\353\241\235.png" differ diff --git "a/12 WEEK/studentManagement/img/1\353\223\261\353\241\235\354\213\244\355\214\250.png" "b/12 WEEK/studentManagement/img/1\353\223\261\353\241\235\354\213\244\355\214\250.png" new file mode 100644 index 00000000..959345a2 Binary files /dev/null and "b/12 WEEK/studentManagement/img/1\353\223\261\353\241\235\354\213\244\355\214\250.png" differ diff --git "a/12 WEEK/studentManagement/img/1\354\210\230\354\240\225.png" "b/12 WEEK/studentManagement/img/1\354\210\230\354\240\225.png" new file mode 100644 index 00000000..bc07c0d9 Binary files /dev/null and "b/12 WEEK/studentManagement/img/1\354\210\230\354\240\225.png" differ diff --git "a/12 WEEK/studentManagement/img/1\354\210\230\354\240\225\354\213\244\355\214\250.png" "b/12 WEEK/studentManagement/img/1\354\210\230\354\240\225\354\213\244\355\214\250.png" new file mode 100644 index 00000000..3a33dd18 Binary files /dev/null and "b/12 WEEK/studentManagement/img/1\354\210\230\354\240\225\354\213\244\355\214\250.png" differ diff --git "a/12 WEEK/studentManagement/img/\353\213\244\354\244\221 \354\202\255\354\240\234.png" "b/12 WEEK/studentManagement/img/\353\213\244\354\244\221 \354\202\255\354\240\234.png" new file mode 100644 index 00000000..c415009c Binary files /dev/null and "b/12 WEEK/studentManagement/img/\353\213\244\354\244\221 \354\202\255\354\240\234.png" differ diff --git "a/12 WEEK/studentManagement/img/\353\213\250\354\235\274 \355\225\231\354\203\235 \354\210\230\354\240\225.png" "b/12 WEEK/studentManagement/img/\353\213\250\354\235\274 \355\225\231\354\203\235 \354\210\230\354\240\225.png" new file mode 100644 index 00000000..3cf49397 Binary files /dev/null and "b/12 WEEK/studentManagement/img/\353\213\250\354\235\274 \355\225\231\354\203\235 \354\210\230\354\240\225.png" differ diff --git "a/12 WEEK/studentManagement/img/\353\213\250\354\235\274\354\202\255\354\240\234.png" "b/12 WEEK/studentManagement/img/\353\213\250\354\235\274\354\202\255\354\240\234.png" new file mode 100644 index 00000000..0ea1cd6b Binary files /dev/null and "b/12 WEEK/studentManagement/img/\353\213\250\354\235\274\354\202\255\354\240\234.png" differ diff --git "a/12 WEEK/studentManagement/img/\353\213\250\354\235\274\354\241\260\355\232\214.png" "b/12 WEEK/studentManagement/img/\353\213\250\354\235\274\354\241\260\355\232\214.png" new file mode 100644 index 00000000..31af01a6 Binary files /dev/null and "b/12 WEEK/studentManagement/img/\353\213\250\354\235\274\354\241\260\355\232\214.png" differ diff --git "a/12 WEEK/studentManagement/img/\353\252\250\353\223\240 \355\225\231\354\203\235 \354\241\260\355\232\214.png" "b/12 WEEK/studentManagement/img/\353\252\250\353\223\240 \355\225\231\354\203\235 \354\241\260\355\232\214.png" new file mode 100644 index 00000000..2ca641f3 Binary files /dev/null and "b/12 WEEK/studentManagement/img/\353\252\250\353\223\240 \355\225\231\354\203\235 \354\241\260\355\232\214.png" differ diff --git "a/12 WEEK/studentManagement/img/\354\212\244\355\201\254\353\246\260\354\203\267 2024-01-24 152533.png" "b/12 WEEK/studentManagement/img/\354\212\244\355\201\254\353\246\260\354\203\267 2024-01-24 152533.png" new file mode 100644 index 00000000..b2d0a4e4 Binary files /dev/null and "b/12 WEEK/studentManagement/img/\354\212\244\355\201\254\353\246\260\354\203\267 2024-01-24 152533.png" differ diff --git "a/12 WEEK/studentManagement/img/\354\264\235 \355\225\231\354\203\235 \354\210\230.png" "b/12 WEEK/studentManagement/img/\354\264\235 \355\225\231\354\203\235 \354\210\230.png" new file mode 100644 index 00000000..62e1559c Binary files /dev/null and "b/12 WEEK/studentManagement/img/\354\264\235 \355\225\231\354\203\235 \354\210\230.png" differ diff --git a/12 WEEK/studentManagement/settings.gradle b/12 WEEK/studentManagement/settings.gradle new file mode 100644 index 00000000..40561028 --- /dev/null +++ b/12 WEEK/studentManagement/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'studentManagement' diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/StudentManagementApplication.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/StudentManagementApplication.java new file mode 100644 index 00000000..cbe3d012 --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/StudentManagementApplication.java @@ -0,0 +1,13 @@ +package fx.studentManagement; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class StudentManagementApplication { + + public static void main(String[] args) { + SpringApplication.run(StudentManagementApplication.class, args); + } + +} diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/ExceptionAdvice.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/ExceptionAdvice.java new file mode 100644 index 00000000..f3f94381 --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/ExceptionAdvice.java @@ -0,0 +1,30 @@ +package fx.studentManagement.common; + +import fx.studentManagement.common.exception.DuplicateStudentException; +import fx.studentManagement.common.exception.NotFoundStudentException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class ExceptionAdvice { + + @ExceptionHandler(DuplicateStudentException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity duplicateStudentException(DuplicateStudentException e) { + log.info("이미 존재하는 회원입니다."); + return new ResponseEntity<>(ResponseMessage.ALREADY_EXIST_STUDENT.getMessage(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(NotFoundStudentException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity notFoundStudentException(NotFoundStudentException e) { + log.info("학생을 조회할 수 없습니다."); + return new ResponseEntity<>(ResponseMessage.NOT_FOUND_STUDENT.getMessage(), HttpStatus.NOT_FOUND); + } + +} diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/ResponseMessage.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/ResponseMessage.java new file mode 100644 index 00000000..faf61e2c --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/ResponseMessage.java @@ -0,0 +1,20 @@ +package fx.studentManagement.common; + +public enum ResponseMessage { + NOT_FOUND_STUDENT("학생을 조회할 수 없습니다."), + ALREADY_EXIST_STUDENT("이미 존재하는 학생입니다."), + SUCCESS_REGISTERED_STUDENT("학생 등록 성공"), + SUCCESS_READ_STUDENT("학생 정보 조회 성공"), + SUCCESS_EDIT_STUDENT_INFO("학생 정보 수정 성공"), + SUCCESS_DELETE_ALL_STUDENT("학생 정보 전체 삭제 성공"), + SUCCESS_DELETE_STUDENT("학생 단일 정보 삭제 성공"); + private final String message; + + ResponseMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/exception/DuplicateStudentException.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/exception/DuplicateStudentException.java new file mode 100644 index 00000000..e3b6809e --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/exception/DuplicateStudentException.java @@ -0,0 +1,22 @@ +package fx.studentManagement.common.exception; + +public class DuplicateStudentException extends RuntimeException { + public DuplicateStudentException() { + } + + public DuplicateStudentException(String message) { + super(message); + } + + public DuplicateStudentException(String message, Throwable cause) { + super(message, cause); + } + + public DuplicateStudentException(Throwable cause) { + super(cause); + } + + public DuplicateStudentException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/exception/NotFoundStudentException.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/exception/NotFoundStudentException.java new file mode 100644 index 00000000..58a6416a --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/common/exception/NotFoundStudentException.java @@ -0,0 +1,22 @@ +package fx.studentManagement.common.exception; + +public class NotFoundStudentException extends RuntimeException { + public NotFoundStudentException() { + } + + public NotFoundStudentException(String message) { + super(message); + } + + public NotFoundStudentException(String message, Throwable cause) { + super(message, cause); + } + + public NotFoundStudentException(Throwable cause) { + super(cause); + } + + public NotFoundStudentException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/StudentController.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/StudentController.java new file mode 100644 index 00000000..ed6483e6 --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/StudentController.java @@ -0,0 +1,91 @@ +package fx.studentManagement.controller; + +import fx.studentManagement.common.ResponseMessage; +import fx.studentManagement.controller.form.EditStudentForm; +import fx.studentManagement.controller.form.SignUpForm; +import fx.studentManagement.entity.Student; +import fx.studentManagement.service.StudentService; +import jakarta.annotation.PostConstruct; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + + +@RestController +@AllArgsConstructor +@RequestMapping("/students") +public class StudentController { + + private final StudentService studentService; + + + @PostMapping //학생 등록 + public ResponseEntity signUp(@Valid @RequestBody SignUpForm signUpForm, BindingResult bindingResult) { + + if(bindingResult.hasErrors()) + return new ResponseEntity<>(bindingResult.getAllErrors(), HttpStatus.BAD_REQUEST); + + studentService.signUp(signUpForm); + return new ResponseEntity<>(ResponseMessage.SUCCESS_REGISTERED_STUDENT.getMessage(), HttpStatus.OK); // 학생 등록 확인을 위해 전체 출력 + } + + @GetMapping("/{studentNumber}") // 단일 학생 조회 + public ResponseEntity showStudent(@PathVariable Long studentNumber) { + return new ResponseEntity<>(studentService.showStudent(studentNumber), HttpStatus.OK); + } + + @GetMapping // 모든 학생 조회 + public ResponseEntity showAllStudent() { + return new ResponseEntity<>(studentService.showAllStudent(), HttpStatus.OK); + } + + @DeleteMapping("/{studentNumber}") // 단일 학생 삭제 + public ResponseEntity deleteStudent(@PathVariable Long studentNumber) { + studentService.deleteStudent(studentNumber); + return new ResponseEntity<>(studentNumber + ResponseMessage.SUCCESS_DELETE_STUDENT.getMessage(), HttpStatus.OK); + } + + @DeleteMapping // 다중 학생 삭제 + public ResponseEntity deleteAllStudent() { + studentService.deleteAllStudent(); + return new ResponseEntity<>(ResponseMessage.SUCCESS_DELETE_ALL_STUDENT.getMessage(), HttpStatus.OK); + } + + @PatchMapping // 단일 학생 정보 수정 + public ResponseEntity editStudentInformation(@Valid @RequestBody EditStudentForm editStudentForm, BindingResult bindingResult) { + + if(bindingResult.hasErrors()) + return new ResponseEntity<>(bindingResult.getAllErrors(), HttpStatus.BAD_REQUEST); + + Student updateStudent = studentService.editStudentInformation(editStudentForm); + return new ResponseEntity<>(updateStudent, HttpStatus.OK); + } + + @GetMapping("/count") // 총 학생 조회 + public int countStudent() { + return studentService.countAllStudent(); + } + + @PostConstruct + public void post() { + Student stu1 = Student.builder() + .studentNumber(2100000L) + .studentName("PARK") + .studentGrade(2) + .studentMajor("SoftwareDept") + .build(); + studentService.signUp(stu1); + + + Student stu2 = Student.builder() + .studentNumber(1900000L) + .studentName("KIM") + .studentGrade(4) + .studentMajor("SoftwareDept") + .build(); + studentService.signUp(stu2); + } +} diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/form/EditStudentForm.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/form/EditStudentForm.java new file mode 100644 index 00000000..8e983182 --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/form/EditStudentForm.java @@ -0,0 +1,24 @@ +package fx.studentManagement.controller.form; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Builder +public class EditStudentForm { + + @Min(value = 1000000) @Max(value = 9999999, message = "- 7자리, 숫자 ( 영문 , 특수 기호 금지 )") + private Long studentNumber; + + @Size(min =2, max = 7) + @Pattern(regexp = "^[가-힣]*$", message = "- 2자리 ~ 7자리 , 한글 ( 영문, 특수기호, 숫자 금지)") + private String studentName; + + // - null 허용 + private String studentAddress; +} diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/form/SignUpForm.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/form/SignUpForm.java new file mode 100644 index 00000000..fe26ae5a --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/controller/form/SignUpForm.java @@ -0,0 +1,38 @@ +package fx.studentManagement.controller.form; + +import fx.studentManagement.entity.enums.Major; +import fx.studentManagement.entity.enums.ValidEnum; +import jakarta.validation.constraints.*; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Builder +public class SignUpForm { +//학번, 이름, 생년월일, 학과, 학기, 주소, 학년 + + @Min(value = 1000000) @Max(value = 9999999, message = "- 7자리, 숫자 ( 영문 , 특수 기호 금지 )") + private Long studentNumber; + + @Size(min =2, max = 7) + @Pattern(regexp = "^[가-힣]*$", message = "- 2자리 ~ 7자리 , 한글 ( 영문, 특수기호, 숫자 금지)") + private String studentName; + + @Pattern(regexp = "\\d{4}_\\d{2}_\\d{2}", message = "- format : YYYY_mm_dd") + private String studentBirth; + + @ValidEnum(enumClass = Major.class, message = "- SoftwareDept, ComputerEngineeringDept, IndustrialDesignDept 외 금지") + private Major studentMajor; + + @Digits(integer = 1, fraction = 0) + @Min(value = 1) @Max(value = 2, message = "- 1학기 , 2학기 외 금지") + private int studentSemester; + + // - null 허용 + private String studentAddress; + + @Digits(integer = 1, fraction = 0) + @Min(value = 1) @Max(value = 5,message = "- 1 학년 ~ 5학년 외 금지") + private int studentGrade; +} diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/Student.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/Student.java new file mode 100644 index 00000000..c1ef288c --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/Student.java @@ -0,0 +1,28 @@ +package fx.studentManagement.entity; + +/* +* 이름, 학번, 학년, 전공 +*/ + +import fx.studentManagement.entity.enums.Major; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@Builder +public class Student { +//학번, 학년, 이름, 생년,생월,생일,학과, 학기,주소 + + private Long studentNumber; //학번 + private int studentGrade; + private String studentName; + private int studentBirthYear; + private int studentBirthMonth; + private int studentBirthDay; + private Major studentMajor; + private int studentSemester; + private String studentAddress; + +} diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/enums/EnumValidator.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/enums/EnumValidator.java new file mode 100644 index 00000000..0c0a0632 --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/enums/EnumValidator.java @@ -0,0 +1,31 @@ +package fx.studentManagement.entity.enums; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/* +* https://jsy1110.github.io/2022/enum-class-validation/ + */ +public class EnumValidator implements ConstraintValidator { + private ValidEnum annotation; + + @Override + public void initialize(ValidEnum constraintAnnotation) { + this.annotation = constraintAnnotation; + } + + @Override + public boolean isValid(Enum value, ConstraintValidatorContext context) { + boolean result = false; + Object[] enumValues = this.annotation.enumClass().getEnumConstants(); + if (enumValues != null) { + for (Object enumValue : enumValues) { + if (value == enumValue) { + result = true; + break; + } + } + } + return result; + } +} \ No newline at end of file diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/enums/Major.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/enums/Major.java new file mode 100644 index 00000000..a6809043 --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/enums/Major.java @@ -0,0 +1,18 @@ +package fx.studentManagement.entity.enums; + +public enum Major { + SoftwareDept("소프트웨어학과"), + ComputerEngineeringDept("컴퓨터공학과"), + IndustrialDesignDept("산업디자인학과"); + + + private String dept; + + Major(String dept) { + this.dept = dept; + } + + public String getDept() { + return dept; + } +} \ No newline at end of file diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/enums/ValidEnum.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/enums/ValidEnum.java new file mode 100644 index 00000000..6b980fb9 --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/entity/enums/ValidEnum.java @@ -0,0 +1,19 @@ +package fx.studentManagement.entity.enums; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = EnumValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidEnum { + String message() default "Invalid value. This is not permitted."; + Class[] groups() default {}; + Class[] payload() default {}; + Class> enumClass(); +} \ No newline at end of file diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/repository/StudentRepository.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/repository/StudentRepository.java new file mode 100644 index 00000000..ba3d92cf --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/repository/StudentRepository.java @@ -0,0 +1,48 @@ +package fx.studentManagement.repository; + +import fx.studentManagement.entity.Student; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Repository +public class StudentRepository { + + private static Map store = new ConcurrentHashMap<>(); + + public Student save(Student student) { + store.put(student.getStudentNumber(), student); + return student; + } + + public Student findByStudentNumber(Long studentNumber) { + return store.get(studentNumber); + } + + public List findAll() { + return new ArrayList<>(store.values()); + } + + public void delteByStudentNumber(Long studentNumber) { + store.remove(studentNumber); + } + + public void deleteAll() { + store.clear(); + } + + public Student updateStudent(Student student) { + return store.replace(student.getStudentNumber(), student); + } + + public int countStudent() { + return store.size(); + } + + public Boolean existsStudent(Long studentNumber) { + return store.get(studentNumber) != null; + } +} diff --git a/12 WEEK/studentManagement/src/main/java/fx/studentManagement/service/StudentService.java b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/service/StudentService.java new file mode 100644 index 00000000..3f1984ad --- /dev/null +++ b/12 WEEK/studentManagement/src/main/java/fx/studentManagement/service/StudentService.java @@ -0,0 +1,108 @@ +package fx.studentManagement.service; + +import fx.studentManagement.common.exception.DuplicateStudentException; +import fx.studentManagement.common.exception.NotFoundStudentException; +import fx.studentManagement.controller.form.EditStudentForm; +import fx.studentManagement.controller.form.SignUpForm; +import fx.studentManagement.entity.Student; +import fx.studentManagement.repository.StudentRepository; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@AllArgsConstructor +@Slf4j +public class StudentService { + + private final StudentRepository studentRepository; + + public void signUp(SignUpForm signUpForm) { //학생 입력 + + if(studentRepository.existsStudent(signUpForm.getStudentNumber())) + throw new DuplicateStudentException(); + + Student student = convertSignUpFormToStudent(signUpForm); + + studentRepository.save(student); + } + + + public Student showStudent(Long studentNumber) { //단일 학생 조회 + if(!studentRepository.existsStudent(studentNumber)) + throw new NotFoundStudentException(); + return studentRepository.findByStudentNumber(studentNumber); + } + + public List showAllStudent() { //모든 학생 조회 + if(studentRepository.findAll() == null) + throw new NotFoundStudentException(); + return studentRepository.findAll(); + } + + public void deleteStudent(Long studentNumber) { // 단일 학생 삭제 + if(!studentRepository.existsStudent(studentNumber)) + throw new NotFoundStudentException(); + studentRepository.delteByStudentNumber(studentNumber); + } + + public void deleteAllStudent() { // 다중 학생 삭제 + studentRepository.deleteAll(); + } + + public Student editStudentInformation(EditStudentForm editStudentForm) { //단일 학생 정보 수정 + + if(!studentRepository.existsStudent(editStudentForm.getStudentNumber())) + throw new NotFoundStudentException(); + + Student existsStudent = studentRepository.findByStudentNumber(editStudentForm.getStudentNumber()); + Student student = convertEditFormToStudent(editStudentForm, existsStudent); + + return studentRepository.updateStudent(student); + } + + public int countAllStudent() { + return studentRepository.countStudent(); + } + + + + + + + private static Student convertSignUpFormToStudent(SignUpForm signUpForm) { + + String[] studentBirth = signUpForm.getStudentBirth().split("_"); + + Student student = Student.builder() + .studentNumber(signUpForm.getStudentNumber()) + .studentGrade(signUpForm.getStudentGrade()) + .studentName(signUpForm.getStudentName()) + .studentBirthYear(Integer.parseInt(studentBirth[0])) + .studentBirthMonth(Integer.parseInt(studentBirth[1])) + .studentBirthDay(Integer.parseInt(studentBirth[2])) + .studentMajor(signUpForm.getStudentMajor()) + .studentSemester(signUpForm.getStudentSemester()) + .studentAddress(signUpForm.getStudentAddress()) + .build(); + return student; + } + + + private static Student convertEditFormToStudent(EditStudentForm editStudentForm, Student existsStudent) { + Student student = Student.builder() + .studentNumber(existsStudent.getStudentNumber()) + .studentGrade(existsStudent.getStudentGrade()) + .studentName(editStudentForm.getStudentName()) + .studentBirthYear(existsStudent.getStudentBirthYear()) + .studentBirthMonth(existsStudent.getStudentBirthMonth()) + .studentBirthDay(existsStudent.getStudentBirthDay()) + .studentMajor(existsStudent.getStudentMajor()) + .studentSemester(existsStudent.getStudentSemester()) + .studentAddress(editStudentForm.getStudentAddress()) + .build(); + return student; + } +} \ No newline at end of file diff --git a/12 WEEK/studentManagement/src/main/resources/application.properties b/12 WEEK/studentManagement/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/12 WEEK/studentManagement/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/12 WEEK/studentManagement/src/test/java/fx/studentManagement/StudentManagementApplicationTests.java b/12 WEEK/studentManagement/src/test/java/fx/studentManagement/StudentManagementApplicationTests.java new file mode 100644 index 00000000..e4d2e483 --- /dev/null +++ b/12 WEEK/studentManagement/src/test/java/fx/studentManagement/StudentManagementApplicationTests.java @@ -0,0 +1,13 @@ +package fx.studentManagement; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class StudentManagementApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/12 WEEK/studentManagement/src/test/java/fx/studentManagement/service/StudentServiceTest.java b/12 WEEK/studentManagement/src/test/java/fx/studentManagement/service/StudentServiceTest.java new file mode 100644 index 00000000..403e2f5f --- /dev/null +++ b/12 WEEK/studentManagement/src/test/java/fx/studentManagement/service/StudentServiceTest.java @@ -0,0 +1,161 @@ +package fx.studentManagement.service; + +import fx.studentManagement.controller.form.EditStudentForm; +import fx.studentManagement.controller.form.SignUpForm; +import fx.studentManagement.entity.Student; +import fx.studentManagement.entity.enums.Major; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + + +@SpringBootTest +class StudentServiceTest { + + @Autowired + private StudentService studentService; + + @BeforeEach + public void BeforeEach() { + SignUpForm stu1 = SignUpForm.builder() + .studentNumber(2100000L) + .studentName("PARK") + .studentBirth("2001_01_01") + .studentMajor(Major.valueOf("SoftwareDept")) + .studentSemester(2) + .studentAddress("충주시") + .studentGrade(1) + .build(); + studentService.signUp(stu1); + + + SignUpForm stu2 = SignUpForm.builder() + .studentNumber(2200000L) + .studentName("KIM") + .studentBirth("2001_01_01") + .studentMajor(Major.valueOf("SoftwareDept")) + .studentSemester(2) + .studentAddress("충주시") + .studentGrade(1) + .build(); + studentService.signUp(stu2); + } + + @AfterEach + public void afterEach() { + studentService.deleteAllStudent(); + } + + @Test + void signUp() { + //give + SignUpForm stu = SignUpForm.builder() + .studentNumber(2300000L) + .studentName("LEE") + .studentBirth("2001_01_01") + .studentMajor(Major.valueOf("SoftwareDept")) + .studentSemester(2) + .studentAddress("충주시") + .studentGrade(3) + .build(); + + //when + studentService.signUp(stu); + + //then + Assertions.assertThat(stu.getStudentNumber()).isEqualTo(2300000L); + Assertions.assertThat(stu.getStudentName()).isEqualTo("LEE"); + Assertions.assertThat(stu.getStudentGrade()).isEqualTo(3); + Assertions.assertThat(stu.getStudentMajor()).isEqualTo(Major.SoftwareDept); + } + + @Test + void showStudent() { + //give + + //when + Student stu1 = studentService.showStudent(2100000L); + Student stu2 = studentService.showStudent(2200000L); + + //then + Assertions.assertThat(stu1.getStudentNumber()).isEqualTo(2100000L); + Assertions.assertThat(stu1.getStudentName()).isEqualTo("PARK"); + Assertions.assertThat(stu1.getStudentGrade()).isEqualTo(1); + Assertions.assertThat(stu1.getStudentMajor()).isEqualTo(Major.SoftwareDept); + + Assertions.assertThat(stu2.getStudentNumber()).isEqualTo(2200000L); + Assertions.assertThat(stu2.getStudentName()).isEqualTo("KIM"); + Assertions.assertThat(stu2.getStudentGrade()).isEqualTo(1); + Assertions.assertThat(stu2.getStudentMajor()).isEqualTo(Major.SoftwareDept); + } + + @Test + void showAllStudent() { + //give + + //when + List students = studentService.showAllStudent(); + //then + Assertions.assertThat(students.size()).isEqualTo(2); + } + + @Test + void deleteStudent() { + //give + + //when + studentService.deleteStudent(2100000L); + List students = studentService.showAllStudent(); + System.out.println("students = " + students); + //then + Assertions.assertThat(students.size()).isEqualTo(1); + + } + + @Test + void deleteAllStudent() { + //give + + //when + studentService.deleteAllStudent(); + List students = studentService.showAllStudent(); + + //then + Assertions.assertThat(students.size()).isEqualTo(0); + + } + + @Test + void updateStudent() { + //give + EditStudentForm updateStu = EditStudentForm.builder() + .studentNumber(2100000L) + .studentName("CHOI") + .studentAddress("서울시") + .build(); + + //when + studentService.editStudentInformation(updateStu); + Student student = studentService.showStudent(2100000L); + + //then + Assertions.assertThat(student.getStudentName()).isEqualTo("CHOI"); + Assertions.assertThat(student.getStudentAddress()).isEqualTo("서울시"); + + } + + @Test + void countAllStudent() { + //give + + //when + int cnt = studentService.countAllStudent(); + //then + Assertions.assertThat(cnt).isEqualTo(2); + } +} \ No newline at end of file diff --git a/2 WEEK/LEE/.gitignore b/2 WEEK/LEE/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/2 WEEK/LEE/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/2 WEEK/LEE/ANSWER.md b/2 WEEK/LEE/ANSWER.md index 066e2c2a..7bd961f4 100644 --- a/2 WEEK/LEE/ANSWER.md +++ b/2 WEEK/LEE/ANSWER.md @@ -45,15 +45,91 @@ https://agilemanifesto.org/iso/ko/manifesto.html
## 주요 이론 요약 -Please provide a summary of your main theory here. + ### SOLID 객체지향 설계 5가지 원칙 + - SRP (Single Responsibility Principle) 단일 책임 원칙 + - 하나의 클래스는 하나의 책임만 가져야한다 + - 클래스를 변경하지 이유는 단 하나여야 한다. 변경이 있을 때 파급 효과가 적어야 한다. + - 이를 지키지 않으면, 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향을 미칠 수 있다. 결국, 유지보수가 매우 비효율적이게 된다. +


+ + - OCP (Open-Closed Principle) 개방-폐쇄 원칙 + - 소프트웨어 요소는 확정에는 열려 있으나 변경에는 닫혀 있어야 한다. + - 즉, 기존의 코드를 변경하지 않고 기능을 수정, 추가할 수 있도록 설계해야한다. + - 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현 + +어떤 모듈의 기능을 수정할 때, 해당 모듈을 이용하는 모든 모듈 또한 수정한다면 유지보수가 복잡해짐. +따라서 OCP를 적용해 기존 코드를 변경하지 않아도 기능을 수정, 추가할 수 있게 해야함 +


+ + - LSP (Liskov Substitution Principle) 리스코프 치환 원칙 + - 하위 타입 객체는 상위 타입 객체에서 가능한 행위를 수행할 수 있어야 한다. + - 즉, 상위 타입 객체를 하위 타입 객체로 대체하여도 정상적으로 동작해야 한다. + - 다형성에서 하위 클래스는 인터페이스의 규약을 다 지켜야 한다. + - 상속 관계에서는 꼭 일반화 관계(IS-A)가 성립해야 한다. + - 상속 관게가 아닌 클래스들을 상속관계로 설정하면, LSP 위반이다. +


+ + - ISP (Interface Segregation Principle) 인터페이스 분리 원칙 + - 클라이언트는 자신이 사용하는 메소드에만 의존해야 한다. + - 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 한 개보다 낫다. + - 인터페이스는 해당 인터페이스를 사용하는 클라이언트를 기준으로 잘게 분리되어야 한다. +


+ + - DIP (Dependency Inversion Principle) 의존 역전 원칙 + 프로 + - 의존 관계를 맺을 때, 변하기 쉬운 구체적인 것 보다는 변하기 어려운 추상적인 것에 의존해야 한다는 것이다. + - 즉, 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다. + - 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다. 구현체에 의존한다면 변경에 어려움이 생긴다 + - 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. + - 저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적이다. +


+ + + + + +### DI : 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉 + - 의존성 주입에는 3가지 방법 존재. + 1. 생성자 주입 (Constructor Injection) + - Spring에서 권장되는 의존 관계 주입 방식 + - 생성자 주입만이 final 키워드를 사용할 수 있음 + - 객체의 불변성이 보장 + 2. Setter 주입 (Setter Injection) + - final 키워드를 사용할 수 없어 불변성이 보장되지 않음 -> 객체가 변할 가능성이 존재 + - JUnit 테스트가 어려워짐 + - 단일책임원칙(SRP) 위반 + 3. 필드주입 (Field Injection) + - 역시 final 키워드 사용 불가 + + - Spring 개발에서 생성자 주입을 사용하기!! ## ISSUE -Please enter your issue details here. +1. RateDiscountPolicy 클래스를 구현했지만 실제 적용하기 위해서는 OrderServiceImpl에서 수정작업을 해주어야 한다. + - OCP 위반 + - FixDiscountPolicy(구현 클래스) 에 의존중임 -> DIP 위반 + +2. calculatePrice 구현하였지만 Order Class의 메소드를 수정하여 NORMAL에는 제대로 된 값이 나오지 않는 문제 ## Solution -Please describe your solution in detail here. +1. OrderServiceImpl에서 생성자를 통해 의존관계를 주입 받도록 코드를 수정한다. + + private final DiscountPolicy discountedPolicy; + + public OrderServiceImpl(DiscountPolicy discountedPolicy) { + this.discountedPolicy = discountedPolicy; + } + +2. 단순 RateDiscountPolicy 에서 return 값 변경으로 해결 + - 생성했던 calculatePrice 메소드 제거 + + + return discountRateAmount; + # 아래처럼 변경 + return price * discountRateAmount / 100; + + ## About diff --git a/2 WEEK/LEE/build.gradle b/2 WEEK/LEE/build.gradle new file mode 100644 index 00000000..48698321 --- /dev/null +++ b/2 WEEK/LEE/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.1.5' + id 'io.spring.dependency-management' version '1.1.3' +} + +group = 'SEOB' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/2 WEEK/LEE/gradle/wrapper/gradle-wrapper.jar b/2 WEEK/LEE/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..7f93135c Binary files /dev/null and b/2 WEEK/LEE/gradle/wrapper/gradle-wrapper.jar differ diff --git a/2 WEEK/LEE/gradle/wrapper/gradle-wrapper.properties b/2 WEEK/LEE/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3fa8f862 --- /dev/null +++ b/2 WEEK/LEE/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/2 WEEK/LEE/gradlew b/2 WEEK/LEE/gradlew new file mode 100644 index 00000000..1aa94a42 --- /dev/null +++ b/2 WEEK/LEE/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/2 WEEK/LEE/gradlew.bat b/2 WEEK/LEE/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/2 WEEK/LEE/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/2 WEEK/LEE/settings.gradle b/2 WEEK/LEE/settings.gradle new file mode 100644 index 00000000..fd561d82 --- /dev/null +++ b/2 WEEK/LEE/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'SEOB' diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/SeobApplication.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/SeobApplication.java new file mode 100644 index 00000000..ad7cb765 --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/SeobApplication.java @@ -0,0 +1,17 @@ +package SEOB.SEOB; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SeobApplication { + + public static void main(String[] args) { + SpringApplication.run(SeobApplication.class, args); + + + + + } + +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/discount/DiscountPolicy.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/discount/DiscountPolicy.java new file mode 100644 index 00000000..d5f4c175 --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/discount/DiscountPolicy.java @@ -0,0 +1,7 @@ +package SEOB.SEOB.discount; + +import SEOB.SEOB.domain.Member; + +public interface DiscountPolicy { + int discount(Member member, int price); +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/discount/FixDiscountPolicy.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/discount/FixDiscountPolicy.java new file mode 100644 index 00000000..89bbaf8b --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/discount/FixDiscountPolicy.java @@ -0,0 +1,17 @@ +package SEOB.SEOB.discount; + +import SEOB.SEOB.domain.GradeType; +import SEOB.SEOB.domain.Member; + +public class FixDiscountPolicy implements DiscountPolicy{ + private final static int discountFixAmount = 1000; + @Override + public int discount(Member member, int price) { + + if(member.getGrade().equals(GradeType.VIP)) //VIP인 경우 + return discountFixAmount; + return 0; + + + } +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/discount/RateDiscountPolicy.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/discount/RateDiscountPolicy.java new file mode 100644 index 00000000..23bf1c33 --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/discount/RateDiscountPolicy.java @@ -0,0 +1,15 @@ +package SEOB.SEOB.discount; + +import SEOB.SEOB.domain.GradeType; +import SEOB.SEOB.domain.Member; + +public class RateDiscountPolicy implements DiscountPolicy{ + private final static int discountRateAmount = 30; + + @Override + public int discount(Member member, int price) { + if(member.getGrade().equals(GradeType.VIP)) + return price * discountRateAmount / 100; + return 0; + } +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/domain/GradeType.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/domain/GradeType.java new file mode 100644 index 00000000..9d66c1e6 --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/domain/GradeType.java @@ -0,0 +1,5 @@ +package SEOB.SEOB.domain; + +public enum GradeType { + NORMAL, VIP +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/domain/Member.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/domain/Member.java new file mode 100644 index 00000000..69260e77 --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/domain/Member.java @@ -0,0 +1,47 @@ +package SEOB.SEOB.domain; + +public class Member { + + private Long id; + private String name; + private GradeType grade; + + + + + //getter setter + public void Member(String name, GradeType grade) { + this.name = name; + this.grade = grade; + } + + public Member(String name, GradeType grade) { + this.name = name; + this.grade = grade; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public GradeType getGrade() { + return grade; + } + + public void setGrade(GradeType grade) { + this.grade = grade; + } + +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/domain/Order.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/domain/Order.java new file mode 100644 index 00000000..bde37bbd --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/domain/Order.java @@ -0,0 +1,52 @@ +package SEOB.SEOB.domain; + +public class Order { + + private Long id; + private String name; + private int price; + private int discountPrice; + + public Order(Long id, String name, int price, int discountPrice) { + this.id = id; + this.name = name; + this.price = price; + this.discountPrice = discountPrice; + } + + public int calculatePrice() { + return price - discountPrice; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getPrice() { + return price; + } + + public void setPrice(int price) { + this.price = price; + } + + public int getDiscountPrice() { + return discountPrice; + } + + public void setDiscountPrice(int discountPrice) { + this.discountPrice = discountPrice; + } +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/MemberRepository.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/MemberRepository.java new file mode 100644 index 00000000..88d0452c --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/MemberRepository.java @@ -0,0 +1,11 @@ +package SEOB.SEOB.repository; + + +import SEOB.SEOB.domain.Member; + +public interface MemberRepository { + public Member save(Member member); + + public Member findById(Long memberId); + +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/MemoryMemberRepository.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/MemoryMemberRepository.java new file mode 100644 index 00000000..d5a2e9f9 --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/MemoryMemberRepository.java @@ -0,0 +1,23 @@ +package SEOB.SEOB.repository; + + +import SEOB.SEOB.domain.Member; + +import java.util.concurrent.ConcurrentHashMap; + +public class MemoryMemberRepository implements MemberRepository{ + + + private static ConcurrentHashMap store = new ConcurrentHashMap<>(); + private static long incrementID = 0L; + + public Member save(Member member) { + member.setId(++incrementID); //호출 시 1씩 증가 + store.put(member.getId(), member); + return member; + } + + public Member findById(Long memberId) { + return store.get(memberId); + } +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/MemoryProductRepository.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/MemoryProductRepository.java new file mode 100644 index 00000000..9d5e47f7 --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/MemoryProductRepository.java @@ -0,0 +1,26 @@ +package SEOB.SEOB.repository; + + +import SEOB.SEOB.domain.Order; + +import java.util.concurrent.ConcurrentHashMap; + +public class MemoryProductRepository implements ProductRepository{ + + //memberId, product + private static ConcurrentHashMap store = new ConcurrentHashMap<>(); + private static long incrementId = 0L; + + + + //키 값으로 멤버의 고유 id 저장 + public Order save(Long memberId, Order order) { + order.setId(++incrementId); + + + store.put(memberId, order); + return order; + } + + +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/ProductRepository.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/ProductRepository.java new file mode 100644 index 00000000..d0f80b04 --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/repository/ProductRepository.java @@ -0,0 +1,10 @@ +package SEOB.SEOB.repository; + + +import SEOB.SEOB.domain.Order; + +public interface ProductRepository { + public Order save(Long memberId, Order order); + + +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/Main.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/Main.java new file mode 100644 index 00000000..4160dc72 --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/Main.java @@ -0,0 +1,56 @@ +package SEOB.SEOB.service; + +import SEOB.SEOB.discount.FixDiscountPolicy; +import SEOB.SEOB.discount.RateDiscountPolicy; +import SEOB.SEOB.domain.GradeType; +import SEOB.SEOB.domain.Member; +import SEOB.SEOB.domain.Order; + + +public class Main { + public static void main(String[] args) { + MemberService memberService = new MemberServiceImpl(); + + //회원 name, grade 지정 + Member member1 = new Member("name1", GradeType.VIP); + Member member2 = new Member("name2", GradeType.NORMAL); + Member member3 = new Member("name3", GradeType.VIP); + + + //회원가입 + Long id1 = memberService.signUp(member1); + Long id2 = memberService.signUp(member2); + Long id3 = memberService.signUp(member3); + System.out.println(id1); //1 + System.out.println(id2); //2 + System.out.println(id3); //3 + + + //id 검색 -> 앞서 지정한 name 출력 + Member findMember1 = memberService.findByName(id1); + Member findMember2 = memberService.findByName(id2); + Member findMember3 = memberService.findByName(id3); + System.out.println(findMember1.getName()); //name1 + System.out.println(findMember2.getName()); //name2 + System.out.println(findMember3.getName()); //name3 + + + //상품 주문 + //고객의 고유 id, 상품, 가격 + OrderService orderServiceFix = new OrderServiceImpl(new FixDiscountPolicy()); + Order order1 = orderServiceFix.createOrder(member1.getId(), "itemA", 10000); //id1 + Order order2 = orderServiceFix.createOrder(member2.getId(), "itemB", 20000); //id2 + + //등급별 할인 가격 확인 + System.out.println("member1 VIP Fix 확인" + order1.getDiscountPrice()); // 1000 + System.out.println("member1 VIP Fix 확인" + order2.getDiscountPrice()); // 0 + + + //RateDiscountPolicy 주입 확인 + OrderService orderServiceRate = new OrderServiceImpl(new RateDiscountPolicy()); + Order order3 = orderServiceRate.createOrder(member3.getId(), "itemC", 10000); //id3 + System.out.println("member1 VIP의 Fix itemA 10000 의 할인된 가격 : " + order1.calculatePrice()); //9000 + System.out.println("member2 NORMAL의 Fix itemB 20000 의 할인된 가격 : " + order2.calculatePrice()); //20000 + System.out.println("member3 VIP의 Rate itemC 10000 의 할인된 가격 : " + order3.calculatePrice()); // 30퍼 할인된 7000 + } +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/MemberService.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/MemberService.java new file mode 100644 index 00000000..a0e7712b --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/MemberService.java @@ -0,0 +1,8 @@ +package SEOB.SEOB.service; + +import SEOB.SEOB.domain.Member; + +public interface MemberService { + public Long signUp(Member member); + public Member findByName(Long memberId); +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/MemberServiceImpl.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/MemberServiceImpl.java new file mode 100644 index 00000000..bda6897f --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/MemberServiceImpl.java @@ -0,0 +1,23 @@ +package SEOB.SEOB.service; + +import SEOB.SEOB.domain.Member; +import SEOB.SEOB.repository.MemberRepository; +import SEOB.SEOB.repository.MemoryMemberRepository; + +public class MemberServiceImpl implements MemberService{ + + + private final MemberRepository memberRepository = new MemoryMemberRepository(); + + + public Long signUp(Member member) { + memberRepository.save(member); + return member.getId(); + } + + public Member findByName(Long memberId) { + Member name = memberRepository.findById(memberId); + return name; + } + +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/OrderService.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/OrderService.java new file mode 100644 index 00000000..cfed4141 --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/OrderService.java @@ -0,0 +1,8 @@ +package SEOB.SEOB.service; + + +import SEOB.SEOB.domain.Order; + +public interface OrderService { + Order createOrder(Long memberId, String name, int price); +} diff --git a/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/OrderServiceImpl.java b/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/OrderServiceImpl.java new file mode 100644 index 00000000..0c688dd5 --- /dev/null +++ b/2 WEEK/LEE/src/main/java/SEOB/SEOB/service/OrderServiceImpl.java @@ -0,0 +1,33 @@ +package SEOB.SEOB.service; + +import SEOB.SEOB.discount.DiscountPolicy; +import SEOB.SEOB.discount.FixDiscountPolicy; +import SEOB.SEOB.domain.GradeType; +import SEOB.SEOB.domain.Member; +import SEOB.SEOB.domain.Order; +import SEOB.SEOB.repository.MemberRepository; +import SEOB.SEOB.repository.MemoryMemberRepository; +import SEOB.SEOB.repository.MemoryProductRepository; +import SEOB.SEOB.repository.ProductRepository; + + +public class OrderServiceImpl implements OrderService{ + + + private final MemberRepository memberRepository = new MemoryMemberRepository(); + private final DiscountPolicy discountPolicy; + /* + * 생성자를 통해 DiscountPolicy를 주입받도록 변경 + */ + public OrderServiceImpl(DiscountPolicy discountPolicy) { + this.discountPolicy = discountPolicy; + } + + public Order createOrder(Long memberId, String name, int price) { + Member member = memberRepository.findById(memberId); + int discountPrice = discountPolicy.discount(member, price); + return new Order(memberId, name, price, discountPrice); + } + + +} \ No newline at end of file diff --git a/2 WEEK/LEE/src/main/resources/application.properties b/2 WEEK/LEE/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/2 WEEK/LEE/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/2 WEEK/LEE/src/test/java/SEOB/SEOB/SeobApplicationTests.java b/2 WEEK/LEE/src/test/java/SEOB/SEOB/SeobApplicationTests.java new file mode 100644 index 00000000..b60e165a --- /dev/null +++ b/2 WEEK/LEE/src/test/java/SEOB/SEOB/SeobApplicationTests.java @@ -0,0 +1,13 @@ +package SEOB.SEOB; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SeobApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/3 WEEK/LEE/core/.gitignore b/3 WEEK/LEE/core/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/3 WEEK/LEE/core/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/3 WEEK/LEE/core/ANSWER.md b/3 WEEK/LEE/core/ANSWER.md new file mode 100644 index 00000000..c6c114d9 --- /dev/null +++ b/3 WEEK/LEE/core/ANSWER.md @@ -0,0 +1,198 @@ +![header](https://capsule-render.vercel.app/api?type=soft&color=auto&height=150§ion=header&text=UserManagement&fontSize=90&animation=blink&align=center) + +-- +## Tech Stack +![Java](https://img.shields.io/badge/Java-ED8B00?style=for-the-badge&logo=openjdk&logoColor=white) +## DB +![Memory](https://img.shields.io/badge/Memory-000000?style=for-the-badge&logo=memory&logoColor=white) +## ORM +![OMR](https://img.shields.io/badge/NONE-000000?style=for-the-badge&logo=NONE&logoColor=white) +## IDE +![intelliJ](https://img.shields.io/badge/IntelliJIDEA-000000?style=for-the-badge&logo=IntelliJIDEA&logoColor=white) +## TEST +![Junit5](https://img.shields.io/badge/JUnit5-25A162?style=for-the-badge&logo=JUnit5&logoColor=white) +## SCM +![GITHUB](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white) +-- +## 요구사항 +[ 새로운 할인 정책 개발 ]

+ +기획자 :
+Service Open 이 일주일 남았지만 `고정 금액 할인` -> `정률 (%) 할인` 으로 변경하고 싶다.
+기존엔 VIP 에게 1000원을 할인해 드렸지만, 10% 할인 정책으로 변경해 주세요.
+ +개발자 :
+일주일 남았는데....
+ +기획자 :
+Agile 선언 모르나요? " 계획을 따르기보다는 변화에 대응하라 "
+https://agilemanifesto.org/iso/ko/manifesto.html
+ +개발자 :
+...
+ +## Study 방법 +[ 😎 Leader's 요구사항 ]
+이전 코드에 `OOP 설계 원칙` 을 위반한 사례를 찾아 README 에 Update 해주세요. +또 발견된 위반 사례를 `OOP 설계 원칙` 을 잘 지켜 수정해 주세요. + +[ 🧐 Member : Study AND ]
+ - main fork 동기화 후 작업 진행 + - 개인 folder 내 에서 작업 할 것 + - ANSWER README 에 작성 하되, 기본 포맷은 기본으로 작성하고, 개별 Custom 후 추가 정보 기입 + +--- + +## 주요 이론 요약 + + ### SOLID 객체지향 설계 5가지 원칙 + - SRP (Single Responsibility Principle) 단일 책임 원칙 + - 하나의 클래스는 하나의 책임만 가져야한다 + - 클래스를 변경하지 이유는 단 하나여야 한다. 변경이 있을 때 파급 효과가 적어야 한다. + - 이를 지키지 않으면, 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향을 미칠 수 있다. 결국, 유지보수가 매우 비효율적이게 된다. +


+ + - OCP (Open-Closed Principle) 개방-폐쇄 원칙 + - 소프트웨어 요소는 확정에는 열려 있으나 변경에는 닫혀 있어야 한다. + - 즉, 기존의 코드를 변경하지 않고 기능을 수정, 추가할 수 있도록 설계해야한다. + - 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현 + +어떤 모듈의 기능을 수정할 때, 해당 모듈을 이용하는 모든 모듈 또한 수정한다면 유지보수가 복잡해짐. +따라서 OCP를 적용해 기존 코드를 변경하지 않아도 기능을 수정, 추가할 수 있게 해야함 +


+ + - LSP (Liskov Substitution Principle) 리스코프 치환 원칙 + - 하위 타입 객체는 상위 타입 객체에서 가능한 행위를 수행할 수 있어야 한다. + - 즉, 상위 타입 객체를 하위 타입 객체로 대체하여도 정상적으로 동작해야 한다. + - 다형성에서 하위 클래스는 인터페이스의 규약을 다 지켜야 한다. + - 상속 관계에서는 꼭 일반화 관계(IS-A)가 성립해야 한다. + - 상속 관게가 아닌 클래스들을 상속관계로 설정하면, LSP 위반이다. +


+ + - ISP (Interface Segregation Principle) 인터페이스 분리 원칙 + - 클라이언트는 자신이 사용하는 메소드에만 의존해야 한다. + - 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 한 개보다 낫다. + - 인터페이스는 해당 인터페이스를 사용하는 클라이언트를 기준으로 잘게 분리되어야 한다. +


+ + - DIP (Dependency Inversion Principle) 의존 역전 원칙 + 프로 + - 의존 관계를 맺을 때, 변하기 쉬운 구체적인 것 보다는 변하기 어려운 추상적인 것에 의존해야 한다는 것이다. + - 즉, 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다. + - 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다. 구현체에 의존한다면 변경에 어려움이 생긴다 + - 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. + - 저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적이다. +


+ + + +--- + +### DI : 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉 + - 의존성 주입에는 3가지 방법 존재. + 1. 생성자 주입 (Constructor Injection) + - Spring에서 권장되는 의존 관계 주입 방식 + - 생성자 주입만이 final 키워드를 사용할 수 있음 + - 객체의 불변성이 보장 + 2. Setter 주입 (Setter Injection) + - final 키워드를 사용할 수 없어 불변성이 보장되지 않음 -> 객체가 변할 가능성이 존재 + - JUnit 테스트가 어려워짐 + - 단일책임원칙(SRP) 위반 + 3. 필드주입 (Field Injection) + - 역시 final 키워드 사용 불가 + + - Spring 개발에서 생성자 주입을 사용하기!! + - DI는 IoC의 한 종류임 + + + +### IoC : 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라, 외부에서 결정되는 것 +- IoC(Inversion of Control)는 "제어의 역전" 이라는 의미로, 말 그대로 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라, 외부에서 셜정되는 것을 의미 +- 객체의 의존성을 역전시켜 객체 간의 결합도를 줄이고 유연한 코드를 작성할 수 있게 하여 가독성 및 코드 중복, 유지 보수를 편하게 할 수 있게 한다. + +- 기존방법(개발자가 직접 의존성을 만듬) + 1. 객체 생성 + 2. 의존성 객체 생성(클래스 내부에 생성) + 3. 의존성 객체 메소드 호출 + +- 스프링에서 + 1. 객체 생성 + 2. 의존성 객체 주입(스스로가 만드는것 x, 제어권을 스프링에게 위임, 스프링이 만들어 놓은 객체 주입) + 3. 의존성 객체 메소드 호출 + +제어의 흐름을 사용자가 컨트롤 하는 것이 아니라 스프링에게 맡겨 작업을 처리하게 된다. + + + +### 스프링 컨테이너 : 스프링에서 자바 객체들을 관리하는 공간을 말함. +- 스프링 컨테이너는 빈의 생성부터 소멸까지 개발자 대신 관리해줌 + +`@Configuration` : 구성정보를 담당하는 것을 설정할 때 @Configuration 를 등록
+`@Bean` : 각 메서드에 @Bean을 등록하면 스프링 컨테이너에 자동으로 등록이 됨. 자바 객체를 Bean 이라고 함. + + + +스프링 컨테이너는 BeanFactory와 ApplicationContext가 있다. +1. `BeanFactory`는 빈을 등록하고 생성하고 조회하고 돌려주는 등 빈을 관리하는 역할을 한다. getBean() 메소드를 통해 빈을 인스턴스화할 수 있다. +2. ` ApplicationContext`는 BeanFactory의 기능을 상속받아 제공한다. + 따라서, 빈을 관리하고 검색하는 기능을 BeanFactory가 제공하고, 그 외의 부가 기능을 제공한다. + - ApplicationContext를 스프링 컨테이너라고 한다.(ApplicationContext는 인터페이스)
+ - 구현할때는 `new AnnotationConfigApplicationContext(클래스이름.class)` 를 사용. + - .getBean(빈 이름, class타입) + +스프링 컨테이너 사용 이유 +- 객체를 생성하기 위해서는 new 생성자를 사용해야함. 그로 인해 애플리케이션에서는 수많은 객체가 존재하고 서로를 참조하게 됨 -> 객체간의 참조가 많으면 많을수록 의존성을 높아짐 +-> 이는 객체지향 프로그래밍과 맞지 않음 +- 따라서 객체 간의 의존성을 낮추어 결합도를 낮추고, 높은 캡슐화를 위해 스프링 컨테이너를 사용한다. + + + + +### 수동 등록과 자동등록의 차이 + +`@Configuration + @Bean`(수동방식) VS `@ComponentScan + @Component`(자동방식)
+1. 수동방식 + - AppConfig.class 에 @Configuration을 적용했다면, 수동으로 각 @Bean을 적용할 메서드를 작성해야함 + - 수동등록은 Config클래스 안에 @Bean을 추가한 메서드로 직접 빈을 등록, 의존성 주입도 여기서 진행함 +2. 자동방식 + - AppConfig.class 에 @Component을 적용했다면, @ComponentScan으로 어떤 패키지부터 Scan을 시작할 건지 작성하고, + 해당 패키지 하위에 @Component로 설정된 클래스가 있다면 클래스들을 SpringContainer에 Bean으로 등록된다. + - 자동등록은 Config 클래스에 @@ComponentScan 을 추가한다. 스프링은 @Component가 붙은 클래스의 객체를 스프링 빈으로 추가한다. + + +## ISSUE + +1. RateDiscountPolicy 클래스를 구현했지만 실제 적용하기 위해서는 OrderServiceImpl에서 수정작업을 해주어야 한다. + - OCP 위반 + - FixDiscountPolicy(구현 클래스) 에 의존중임 -> DIP 위반 + +2. calculatePrice 구현하였지만 Order Class의 메소드를 수정하여 NORMAL에는 제대로 된 값이 나오지 않는 문제 + +## Solution + +1. OrderServiceImpl에서 생성자를 통해 의존관계를 주입 받도록 코드를 수정한다. + + private final DiscountPolicy discountedPolicy; + + public OrderServiceImpl(DiscountPolicy discountedPolicy) { + this.discountedPolicy = discountedPolicy; + } + +2. 단순 RateDiscountPolicy 에서 return 값 변경으로 해결 + - 생성했던 calculatePrice 메소드 제거 + + + return discountRateAmount; + # 아래처럼 변경 + return price * discountRateAmount / 100; + + + +## About + +Please enter your personal feelings, what you learned, and what you need to learn here. + +## Question To Reader + +After completing the mission, please enter any suggestions or questions. + diff --git a/3 WEEK/LEE/core/build.gradle b/3 WEEK/LEE/core/build.gradle new file mode 100644 index 00000000..a0012c9e --- /dev/null +++ b/3 WEEK/LEE/core/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.1.5' + id 'io.spring.dependency-management' version '1.1.3' +} + +group = 'hello' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +dependencies { + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/3 WEEK/LEE/core/gradle/wrapper/gradle-wrapper.jar b/3 WEEK/LEE/core/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..7f93135c Binary files /dev/null and b/3 WEEK/LEE/core/gradle/wrapper/gradle-wrapper.jar differ diff --git a/3 WEEK/LEE/core/gradle/wrapper/gradle-wrapper.properties b/3 WEEK/LEE/core/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3fa8f862 --- /dev/null +++ b/3 WEEK/LEE/core/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/3 WEEK/LEE/core/gradlew b/3 WEEK/LEE/core/gradlew new file mode 100644 index 00000000..1aa94a42 --- /dev/null +++ b/3 WEEK/LEE/core/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/3 WEEK/LEE/core/gradlew.bat b/3 WEEK/LEE/core/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/3 WEEK/LEE/core/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/3 WEEK/LEE/core/settings.gradle b/3 WEEK/LEE/core/settings.gradle new file mode 100644 index 00000000..4d52ac57 --- /dev/null +++ b/3 WEEK/LEE/core/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'core' diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/AppConfig.java b/3 WEEK/LEE/core/src/main/java/hello/core/AppConfig.java new file mode 100644 index 00000000..ff4b722e --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/AppConfig.java @@ -0,0 +1,41 @@ +package hello.core; + +import hello.core.discount.DiscountPolicy; +import hello.core.discount.RateDiscountPolicy; +import hello.core.member.MemberRepository; +import hello.core.member.MemberService; +import hello.core.member.MemberServiceImpl; +import hello.core.member.MemoryMemberRepository; +import hello.core.order.OrderService; +import hello.core.order.OrderServiceImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { + + @Bean + public MemberService memberService(){ + return new MemberServiceImpl(memberRepository()); + } + + @Bean + public OrderService orderService(){ + return new OrderServiceImpl( + memberRepository(), + discountPolicy() + ); + } + + @Bean + public MemberRepository memberRepository(){ + return new MemoryMemberRepository(); + } + + @Bean + public DiscountPolicy discountPolicy(){ + //return new FixDiscountPolicy(); + return new RateDiscountPolicy(); + } + +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/CoreApplication.java b/3 WEEK/LEE/core/src/main/java/hello/core/CoreApplication.java new file mode 100644 index 00000000..5bc183f2 --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/CoreApplication.java @@ -0,0 +1,13 @@ +package hello.core; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CoreApplication { + + public static void main(String[] args) { + SpringApplication.run(CoreApplication.class, args); + } + +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/MemberApp.java b/3 WEEK/LEE/core/src/main/java/hello/core/MemberApp.java new file mode 100644 index 00000000..2ab3e8fa --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/MemberApp.java @@ -0,0 +1,30 @@ +package hello.core; + +import hello.core.member.Grade; +import hello.core.member.Member; +import hello.core.member.MemberService; +import hello.core.member.MemberServiceImpl; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + + +public class MemberApp { + + public static void main(String[] args) { + + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); + MemberService memberService = applicationContext.getBean("memberService",MemberService.class); + + Member member = new Member("memberA", Grade.VIP); + + memberService.join(member); + + Member findMember = memberService.findMember(1L); + + System.out.println("member = " + member.getName()); + System.out.println("findMember = " + findMember.getName()); + + + } + +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/OrderApp.java b/3 WEEK/LEE/core/src/main/java/hello/core/OrderApp.java new file mode 100644 index 00000000..2e4f4bb2 --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/OrderApp.java @@ -0,0 +1,27 @@ +package hello.core; + +import hello.core.member.Grade; +import hello.core.member.Member; +import hello.core.member.MemberService; +import hello.core.member.MemberServiceImpl; +import hello.core.order.Order; +import hello.core.order.OrderService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +public class OrderApp { + public static void main(String[] args) { + + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); + MemberService memberService = applicationContext.getBean("memberService",MemberService.class); + OrderService orderService = applicationContext.getBean("orderService",OrderService.class); + + Member member = new Member("memberA", Grade.VIP); + memberService.join(member); + + Order order = orderService.createOrder(member.getId(),"itemA",10000); + + System.out.println("order = " + order); + + } +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/discount/DiscountPolicy.java b/3 WEEK/LEE/core/src/main/java/hello/core/discount/DiscountPolicy.java new file mode 100644 index 00000000..2b8230ce --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/discount/DiscountPolicy.java @@ -0,0 +1,9 @@ +package hello.core.discount; + +import hello.core.member.Member; + +public interface DiscountPolicy { + + int discount(Member member , int price); + +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/discount/FixDiscountPolicy.java b/3 WEEK/LEE/core/src/main/java/hello/core/discount/FixDiscountPolicy.java new file mode 100644 index 00000000..030b1ed2 --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/discount/FixDiscountPolicy.java @@ -0,0 +1,20 @@ +package hello.core.discount; + +import hello.core.member.Grade; +import hello.core.member.Member; + +public class FixDiscountPolicy implements DiscountPolicy{ + + private int discountFixAmount = 1000; + + @Override + public int discount(Member member, int price) { + + if (member.getGrade() == Grade.VIP) { + return discountFixAmount; + }else { + return 0; + } + + } +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/discount/RateDiscountPolicy.java b/3 WEEK/LEE/core/src/main/java/hello/core/discount/RateDiscountPolicy.java new file mode 100644 index 00000000..f99b55e4 --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/discount/RateDiscountPolicy.java @@ -0,0 +1,20 @@ +package hello.core.discount; + +import hello.core.member.Grade; +import hello.core.member.Member; + +public class RateDiscountPolicy implements DiscountPolicy{ + + private int discountPercent = 10; + + @Override + public int discount(Member member, int price) { + + if (member.getGrade() == Grade.VIP){ + return price * discountPercent / 100; + } + + return 0; + + } +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/member/Grade.java b/3 WEEK/LEE/core/src/main/java/hello/core/member/Grade.java new file mode 100644 index 00000000..488c7490 --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/member/Grade.java @@ -0,0 +1,8 @@ +package hello.core.member; + +public enum Grade { + + BASIC, + VIP + +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/member/Member.java b/3 WEEK/LEE/core/src/main/java/hello/core/member/Member.java new file mode 100644 index 00000000..6f74e2ad --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/member/Member.java @@ -0,0 +1,18 @@ +package hello.core.member; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Member { + + private Long id; + private String name; + private Grade grade; + + public Member( String name, Grade grade) { + this.name = name; + this.grade = grade; + } +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/member/MemberRepository.java b/3 WEEK/LEE/core/src/main/java/hello/core/member/MemberRepository.java new file mode 100644 index 00000000..1b23df5e --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/member/MemberRepository.java @@ -0,0 +1,9 @@ +package hello.core.member; + +public interface MemberRepository { + + void save(Member member); + + Member findById(Long memberId); + +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/member/MemberService.java b/3 WEEK/LEE/core/src/main/java/hello/core/member/MemberService.java new file mode 100644 index 00000000..9101fe64 --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/member/MemberService.java @@ -0,0 +1,9 @@ +package hello.core.member; + +public interface MemberService { + + void join(Member member); + + Member findMember(Long memberId); + +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/member/MemberServiceImpl.java b/3 WEEK/LEE/core/src/main/java/hello/core/member/MemberServiceImpl.java new file mode 100644 index 00000000..f0f93670 --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/member/MemberServiceImpl.java @@ -0,0 +1,20 @@ +package hello.core.member; + +public class MemberServiceImpl implements MemberService{ + + private final MemberRepository memberRepository; + + public MemberServiceImpl(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public void join(Member member) { + memberRepository.save(member); + } + + @Override + public Member findMember(Long memberId) { + return memberRepository.findById(memberId); + } +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/member/MemoryMemberRepository.java b/3 WEEK/LEE/core/src/main/java/hello/core/member/MemoryMemberRepository.java new file mode 100644 index 00000000..3acc3598 --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/member/MemoryMemberRepository.java @@ -0,0 +1,22 @@ +package hello.core.member; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class MemoryMemberRepository implements MemberRepository { + + private static ConcurrentHashMap store = new ConcurrentHashMap<>(); + private static long SEQUENCE = 0L; + + @Override + public void save(Member member) { + member.setId(++SEQUENCE); + store.put(member.getId(),member); + } + + @Override + public Member findById(Long memberId) { + return store.get(memberId); + } +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/order/Order.java b/3 WEEK/LEE/core/src/main/java/hello/core/order/Order.java new file mode 100644 index 00000000..1fbc0321 --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/order/Order.java @@ -0,0 +1,24 @@ +package hello.core.order; + +import lombok.AllArgsConstructor; +import lombok.ToString; + +@ToString +@AllArgsConstructor +public class Order { + + private Long memberId; + private String itemName; + private int itemPrice; + private int discountPrice; + + + public int calculatePrice(){ + return itemPrice - discountPrice; + } + + public int getDiscountPrice(){ + return discountPrice; + } + +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/order/OrderService.java b/3 WEEK/LEE/core/src/main/java/hello/core/order/OrderService.java new file mode 100644 index 00000000..c408fb95 --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/order/OrderService.java @@ -0,0 +1,5 @@ +package hello.core.order; + +public interface OrderService { + Order createOrder(Long memberId, String itemName, int itemPrice); +} diff --git a/3 WEEK/LEE/core/src/main/java/hello/core/order/OrderServiceImpl.java b/3 WEEK/LEE/core/src/main/java/hello/core/order/OrderServiceImpl.java new file mode 100644 index 00000000..214b205a --- /dev/null +++ b/3 WEEK/LEE/core/src/main/java/hello/core/order/OrderServiceImpl.java @@ -0,0 +1,23 @@ +package hello.core.order; + +import hello.core.discount.DiscountPolicy; +import hello.core.member.Member; +import hello.core.member.MemberRepository; + +public class OrderServiceImpl implements OrderService{ + + private final MemberRepository memberRepository; + private final DiscountPolicy discountPolicy; + + public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { + this.memberRepository = memberRepository; + this.discountPolicy = discountPolicy; + } + + @Override + public Order createOrder(Long memberId, String itemName, int itemPrice) { + Member member = memberRepository.findById(memberId); + int discountPrice = discountPolicy.discount(member,itemPrice); + return new Order(memberId, itemName, itemPrice, discountPrice); + } +} diff --git a/3 WEEK/LEE/core/src/main/resources/application.properties b/3 WEEK/LEE/core/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/3 WEEK/LEE/core/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/3 WEEK/LEE/core/src/test/java/hello/core/CoreApplicationTests.java b/3 WEEK/LEE/core/src/test/java/hello/core/CoreApplicationTests.java new file mode 100644 index 00000000..90937b13 --- /dev/null +++ b/3 WEEK/LEE/core/src/test/java/hello/core/CoreApplicationTests.java @@ -0,0 +1,13 @@ +package hello.core; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CoreApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/3 WEEK/LEE/core/src/test/java/hello/core/discount/RateDiscountPolicyTest.java b/3 WEEK/LEE/core/src/test/java/hello/core/discount/RateDiscountPolicyTest.java new file mode 100644 index 00000000..bc3807d6 --- /dev/null +++ b/3 WEEK/LEE/core/src/test/java/hello/core/discount/RateDiscountPolicyTest.java @@ -0,0 +1,42 @@ +package hello.core.discount; + +import hello.core.member.Grade; +import hello.core.member.Member; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class RateDiscountPolicyTest { + + RateDiscountPolicy discountPolicy = new RateDiscountPolicy(); + + @Test + @DisplayName("VIP 10% 할인") + void vip_o(){ + + //given + Member member = new Member("memberVIP", Grade.VIP); + //when + int discount = discountPolicy.discount(member,10000); + //then + assertThat(discount).isEqualTo(1000); + + + } + + @Test + @DisplayName("VIP가 아니면 할인이 적용 안됨") + void vip_x(){ + + //given + Member member = new Member("memberBasic",Grade.BASIC); + //when + int discount = discountPolicy.discount(member,10000); + //then + assertThat(discount).isEqualTo(0); + + } + + +} \ No newline at end of file diff --git a/3 WEEK/LEE/core/src/test/java/hello/core/member/MemberServiceTest.java b/3 WEEK/LEE/core/src/test/java/hello/core/member/MemberServiceTest.java new file mode 100644 index 00000000..388806f3 --- /dev/null +++ b/3 WEEK/LEE/core/src/test/java/hello/core/member/MemberServiceTest.java @@ -0,0 +1,34 @@ +package hello.core.member; + +import hello.core.AppConfig; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class MemberServiceTest { + + MemberService memberService; + + @BeforeEach + public void beforeEach(){ + AppConfig appConfig = new AppConfig(); + memberService = appConfig.memberService(); + } + + @Test + void join() { + //given + Member member = new Member("memberA",Grade.VIP); + + //when + memberService.join(member); + Member findMember = memberService.findMember(1L); + + //then + assertThat(member).isEqualTo(findMember); + + } +} \ No newline at end of file diff --git a/3 WEEK/LEE/core/src/test/java/hello/core/order/OrderServiceTest.java b/3 WEEK/LEE/core/src/test/java/hello/core/order/OrderServiceTest.java new file mode 100644 index 00000000..50167334 --- /dev/null +++ b/3 WEEK/LEE/core/src/test/java/hello/core/order/OrderServiceTest.java @@ -0,0 +1,37 @@ +package hello.core.order; + +import hello.core.AppConfig; +import hello.core.member.Grade; +import hello.core.member.Member; +import hello.core.member.MemberService; +import hello.core.member.MemberServiceImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.internal.matchers.Or; + +import static org.junit.jupiter.api.Assertions.*; + +class OrderServiceTest { + + MemberService memberService; + OrderService orderService; + + @BeforeEach + public void beforeEach(){ + AppConfig appConfig = new AppConfig(); + memberService = appConfig.memberService(); + orderService = appConfig.orderService(); + } + + @Test + void createOrder() { + + Member member = new Member("memberA", Grade.VIP); + memberService.join(member); + + Order order = orderService.createOrder(member.getId(),"itemA",10000); + org.assertj.core.api.Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000); + + } +} \ No newline at end of file diff --git a/4 WEEK/item-service/.gitignore b/4 WEEK/item-service/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/4 WEEK/item-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/4 WEEK/item-service/ANSWER.md b/4 WEEK/item-service/ANSWER.md new file mode 100644 index 00000000..c6c114d9 --- /dev/null +++ b/4 WEEK/item-service/ANSWER.md @@ -0,0 +1,198 @@ +![header](https://capsule-render.vercel.app/api?type=soft&color=auto&height=150§ion=header&text=UserManagement&fontSize=90&animation=blink&align=center) + +-- +## Tech Stack +![Java](https://img.shields.io/badge/Java-ED8B00?style=for-the-badge&logo=openjdk&logoColor=white) +## DB +![Memory](https://img.shields.io/badge/Memory-000000?style=for-the-badge&logo=memory&logoColor=white) +## ORM +![OMR](https://img.shields.io/badge/NONE-000000?style=for-the-badge&logo=NONE&logoColor=white) +## IDE +![intelliJ](https://img.shields.io/badge/IntelliJIDEA-000000?style=for-the-badge&logo=IntelliJIDEA&logoColor=white) +## TEST +![Junit5](https://img.shields.io/badge/JUnit5-25A162?style=for-the-badge&logo=JUnit5&logoColor=white) +## SCM +![GITHUB](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white) +-- +## 요구사항 +[ 새로운 할인 정책 개발 ]

+ +기획자 :
+Service Open 이 일주일 남았지만 `고정 금액 할인` -> `정률 (%) 할인` 으로 변경하고 싶다.
+기존엔 VIP 에게 1000원을 할인해 드렸지만, 10% 할인 정책으로 변경해 주세요.
+ +개발자 :
+일주일 남았는데....
+ +기획자 :
+Agile 선언 모르나요? " 계획을 따르기보다는 변화에 대응하라 "
+https://agilemanifesto.org/iso/ko/manifesto.html
+ +개발자 :
+...
+ +## Study 방법 +[ 😎 Leader's 요구사항 ]
+이전 코드에 `OOP 설계 원칙` 을 위반한 사례를 찾아 README 에 Update 해주세요. +또 발견된 위반 사례를 `OOP 설계 원칙` 을 잘 지켜 수정해 주세요. + +[ 🧐 Member : Study AND ]
+ - main fork 동기화 후 작업 진행 + - 개인 folder 내 에서 작업 할 것 + - ANSWER README 에 작성 하되, 기본 포맷은 기본으로 작성하고, 개별 Custom 후 추가 정보 기입 + +--- + +## 주요 이론 요약 + + ### SOLID 객체지향 설계 5가지 원칙 + - SRP (Single Responsibility Principle) 단일 책임 원칙 + - 하나의 클래스는 하나의 책임만 가져야한다 + - 클래스를 변경하지 이유는 단 하나여야 한다. 변경이 있을 때 파급 효과가 적어야 한다. + - 이를 지키지 않으면, 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향을 미칠 수 있다. 결국, 유지보수가 매우 비효율적이게 된다. +


+ + - OCP (Open-Closed Principle) 개방-폐쇄 원칙 + - 소프트웨어 요소는 확정에는 열려 있으나 변경에는 닫혀 있어야 한다. + - 즉, 기존의 코드를 변경하지 않고 기능을 수정, 추가할 수 있도록 설계해야한다. + - 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현 + +어떤 모듈의 기능을 수정할 때, 해당 모듈을 이용하는 모든 모듈 또한 수정한다면 유지보수가 복잡해짐. +따라서 OCP를 적용해 기존 코드를 변경하지 않아도 기능을 수정, 추가할 수 있게 해야함 +


+ + - LSP (Liskov Substitution Principle) 리스코프 치환 원칙 + - 하위 타입 객체는 상위 타입 객체에서 가능한 행위를 수행할 수 있어야 한다. + - 즉, 상위 타입 객체를 하위 타입 객체로 대체하여도 정상적으로 동작해야 한다. + - 다형성에서 하위 클래스는 인터페이스의 규약을 다 지켜야 한다. + - 상속 관계에서는 꼭 일반화 관계(IS-A)가 성립해야 한다. + - 상속 관게가 아닌 클래스들을 상속관계로 설정하면, LSP 위반이다. +


+ + - ISP (Interface Segregation Principle) 인터페이스 분리 원칙 + - 클라이언트는 자신이 사용하는 메소드에만 의존해야 한다. + - 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 한 개보다 낫다. + - 인터페이스는 해당 인터페이스를 사용하는 클라이언트를 기준으로 잘게 분리되어야 한다. +


+ + - DIP (Dependency Inversion Principle) 의존 역전 원칙 + 프로 + - 의존 관계를 맺을 때, 변하기 쉬운 구체적인 것 보다는 변하기 어려운 추상적인 것에 의존해야 한다는 것이다. + - 즉, 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다. + - 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다. 구현체에 의존한다면 변경에 어려움이 생긴다 + - 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. + - 저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적이다. +


+ + + +--- + +### DI : 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉 + - 의존성 주입에는 3가지 방법 존재. + 1. 생성자 주입 (Constructor Injection) + - Spring에서 권장되는 의존 관계 주입 방식 + - 생성자 주입만이 final 키워드를 사용할 수 있음 + - 객체의 불변성이 보장 + 2. Setter 주입 (Setter Injection) + - final 키워드를 사용할 수 없어 불변성이 보장되지 않음 -> 객체가 변할 가능성이 존재 + - JUnit 테스트가 어려워짐 + - 단일책임원칙(SRP) 위반 + 3. 필드주입 (Field Injection) + - 역시 final 키워드 사용 불가 + + - Spring 개발에서 생성자 주입을 사용하기!! + - DI는 IoC의 한 종류임 + + + +### IoC : 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라, 외부에서 결정되는 것 +- IoC(Inversion of Control)는 "제어의 역전" 이라는 의미로, 말 그대로 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라, 외부에서 셜정되는 것을 의미 +- 객체의 의존성을 역전시켜 객체 간의 결합도를 줄이고 유연한 코드를 작성할 수 있게 하여 가독성 및 코드 중복, 유지 보수를 편하게 할 수 있게 한다. + +- 기존방법(개발자가 직접 의존성을 만듬) + 1. 객체 생성 + 2. 의존성 객체 생성(클래스 내부에 생성) + 3. 의존성 객체 메소드 호출 + +- 스프링에서 + 1. 객체 생성 + 2. 의존성 객체 주입(스스로가 만드는것 x, 제어권을 스프링에게 위임, 스프링이 만들어 놓은 객체 주입) + 3. 의존성 객체 메소드 호출 + +제어의 흐름을 사용자가 컨트롤 하는 것이 아니라 스프링에게 맡겨 작업을 처리하게 된다. + + + +### 스프링 컨테이너 : 스프링에서 자바 객체들을 관리하는 공간을 말함. +- 스프링 컨테이너는 빈의 생성부터 소멸까지 개발자 대신 관리해줌 + +`@Configuration` : 구성정보를 담당하는 것을 설정할 때 @Configuration 를 등록
+`@Bean` : 각 메서드에 @Bean을 등록하면 스프링 컨테이너에 자동으로 등록이 됨. 자바 객체를 Bean 이라고 함. + + + +스프링 컨테이너는 BeanFactory와 ApplicationContext가 있다. +1. `BeanFactory`는 빈을 등록하고 생성하고 조회하고 돌려주는 등 빈을 관리하는 역할을 한다. getBean() 메소드를 통해 빈을 인스턴스화할 수 있다. +2. ` ApplicationContext`는 BeanFactory의 기능을 상속받아 제공한다. + 따라서, 빈을 관리하고 검색하는 기능을 BeanFactory가 제공하고, 그 외의 부가 기능을 제공한다. + - ApplicationContext를 스프링 컨테이너라고 한다.(ApplicationContext는 인터페이스)
+ - 구현할때는 `new AnnotationConfigApplicationContext(클래스이름.class)` 를 사용. + - .getBean(빈 이름, class타입) + +스프링 컨테이너 사용 이유 +- 객체를 생성하기 위해서는 new 생성자를 사용해야함. 그로 인해 애플리케이션에서는 수많은 객체가 존재하고 서로를 참조하게 됨 -> 객체간의 참조가 많으면 많을수록 의존성을 높아짐 +-> 이는 객체지향 프로그래밍과 맞지 않음 +- 따라서 객체 간의 의존성을 낮추어 결합도를 낮추고, 높은 캡슐화를 위해 스프링 컨테이너를 사용한다. + + + + +### 수동 등록과 자동등록의 차이 + +`@Configuration + @Bean`(수동방식) VS `@ComponentScan + @Component`(자동방식)
+1. 수동방식 + - AppConfig.class 에 @Configuration을 적용했다면, 수동으로 각 @Bean을 적용할 메서드를 작성해야함 + - 수동등록은 Config클래스 안에 @Bean을 추가한 메서드로 직접 빈을 등록, 의존성 주입도 여기서 진행함 +2. 자동방식 + - AppConfig.class 에 @Component을 적용했다면, @ComponentScan으로 어떤 패키지부터 Scan을 시작할 건지 작성하고, + 해당 패키지 하위에 @Component로 설정된 클래스가 있다면 클래스들을 SpringContainer에 Bean으로 등록된다. + - 자동등록은 Config 클래스에 @@ComponentScan 을 추가한다. 스프링은 @Component가 붙은 클래스의 객체를 스프링 빈으로 추가한다. + + +## ISSUE + +1. RateDiscountPolicy 클래스를 구현했지만 실제 적용하기 위해서는 OrderServiceImpl에서 수정작업을 해주어야 한다. + - OCP 위반 + - FixDiscountPolicy(구현 클래스) 에 의존중임 -> DIP 위반 + +2. calculatePrice 구현하였지만 Order Class의 메소드를 수정하여 NORMAL에는 제대로 된 값이 나오지 않는 문제 + +## Solution + +1. OrderServiceImpl에서 생성자를 통해 의존관계를 주입 받도록 코드를 수정한다. + + private final DiscountPolicy discountedPolicy; + + public OrderServiceImpl(DiscountPolicy discountedPolicy) { + this.discountedPolicy = discountedPolicy; + } + +2. 단순 RateDiscountPolicy 에서 return 값 변경으로 해결 + - 생성했던 calculatePrice 메소드 제거 + + + return discountRateAmount; + # 아래처럼 변경 + return price * discountRateAmount / 100; + + + +## About + +Please enter your personal feelings, what you learned, and what you need to learn here. + +## Question To Reader + +After completing the mission, please enter any suggestions or questions. + diff --git a/4 WEEK/item-service/build.gradle b/4 WEEK/item-service/build.gradle new file mode 100644 index 00000000..1ac9f713 --- /dev/null +++ b/4 WEEK/item-service/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.0' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'hello' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/4 WEEK/item-service/gradle/wrapper/gradle-wrapper.jar b/4 WEEK/item-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..7f93135c Binary files /dev/null and b/4 WEEK/item-service/gradle/wrapper/gradle-wrapper.jar differ diff --git a/4 WEEK/item-service/gradle/wrapper/gradle-wrapper.properties b/4 WEEK/item-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3fa8f862 --- /dev/null +++ b/4 WEEK/item-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/4 WEEK/item-service/gradlew b/4 WEEK/item-service/gradlew new file mode 100644 index 00000000..1aa94a42 --- /dev/null +++ b/4 WEEK/item-service/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/4 WEEK/item-service/gradlew.bat b/4 WEEK/item-service/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/4 WEEK/item-service/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/4 WEEK/item-service/settings.gradle b/4 WEEK/item-service/settings.gradle new file mode 100644 index 00000000..df5bd80b --- /dev/null +++ b/4 WEEK/item-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'item-service' diff --git a/4 WEEK/item-service/src/main/java/hello/itemservice/AppConfig.java b/4 WEEK/item-service/src/main/java/hello/itemservice/AppConfig.java new file mode 100644 index 00000000..fae6a615 --- /dev/null +++ b/4 WEEK/item-service/src/main/java/hello/itemservice/AppConfig.java @@ -0,0 +1,24 @@ +//package hello.itemservice; +// +//import hello.itemservice.repository.ItemRepository; +//import hello.itemservice.repository.MemoryItemRepository; +//import hello.itemservice.service.ItemService; +//import hello.itemservice.service.ItemServiceImpl; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +// +//@Configuration +//public class AppConfig { +// +// @Bean +// public ItemService itemService(){ +// return new ItemServiceImpl(itemRepository()); +// } +// +// @Bean +// public ItemRepository itemRepository(){ +// return new MemoryItemRepository(); +// } +// +//} + diff --git a/4 WEEK/item-service/src/main/java/hello/itemservice/ItemServiceApplication.java b/4 WEEK/item-service/src/main/java/hello/itemservice/ItemServiceApplication.java new file mode 100644 index 00000000..1311934b --- /dev/null +++ b/4 WEEK/item-service/src/main/java/hello/itemservice/ItemServiceApplication.java @@ -0,0 +1,13 @@ +package hello.itemservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ItemServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(ItemServiceApplication.class, args); + } + +} diff --git a/4 WEEK/item-service/src/main/java/hello/itemservice/controller/HomeController.java b/4 WEEK/item-service/src/main/java/hello/itemservice/controller/HomeController.java new file mode 100644 index 00000000..4510e6d0 --- /dev/null +++ b/4 WEEK/item-service/src/main/java/hello/itemservice/controller/HomeController.java @@ -0,0 +1,28 @@ +package hello.itemservice.controller; + +import hello.itemservice.domain.Item; +import hello.itemservice.service.ItemService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.List; + +@Controller +public class HomeController { + + private ItemService itemService; + @Autowired + public HomeController(ItemService itemService) { + this.itemService = itemService; + } + + @GetMapping("/") + public String home(Model model) { + List findAll = itemService.findAll(); + model.addAttribute("findAll", findAll); + System.out.println("MODEL : "+model); + return "home"; + } +} diff --git a/4 WEEK/item-service/src/main/java/hello/itemservice/controller/ItemController.java b/4 WEEK/item-service/src/main/java/hello/itemservice/controller/ItemController.java new file mode 100644 index 00000000..a80f895f --- /dev/null +++ b/4 WEEK/item-service/src/main/java/hello/itemservice/controller/ItemController.java @@ -0,0 +1,71 @@ +package hello.itemservice.controller; + + +import hello.itemservice.domain.Item; +import hello.itemservice.repository.ItemRepository; +import hello.itemservice.service.ItemService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; + +@Controller +public class ItemController { + + private ItemService itemService; + private ItemRepository itemRepository; + @Autowired + public ItemController(ItemService itemService, ItemRepository itemRepository) { + this.itemService = itemService; + this.itemRepository = itemRepository; + } + + @GetMapping("/addItem") + public String addItemForm() { + return "/addItemForm"; + } + + @PostMapping("/addItem") + public String addItem(ItemForm form) { + //생성자로 주입 + Item item = new Item( + form.getItemName(), + form.getPrice(), + form.getQuantity() + ); + + itemService.addItem(item); + + return "redirect:/"; + } + + //상품상세 + @GetMapping("/{itemId}") + public String item(@PathVariable long itemId, Model model) { + Item item = itemRepository.findByItemId(itemId); + model.addAttribute("item", item); + return "/itemDetailForm"; + } + + //상품 수정 + @GetMapping("/{itemId}/editItem") + public String editItemForm(@PathVariable("itemId") long itemId, Model model) { + Item findById = itemRepository.findByItemId(itemId); + + model.addAttribute("item", findById); + return "/editItemForm"; + } + + @PostMapping("/{itemId}/editItem") + public String editItemForm(@PathVariable("itemId") long itemId, ItemForm form) { + Item item = new Item( + form.getItemName(), + form.getPrice(), + form.getQuantity() + ); + itemService.editItem(itemId, item); + return "redirect:/"; + } +} diff --git a/4 WEEK/item-service/src/main/java/hello/itemservice/controller/ItemForm.java b/4 WEEK/item-service/src/main/java/hello/itemservice/controller/ItemForm.java new file mode 100644 index 00000000..18a58f61 --- /dev/null +++ b/4 WEEK/item-service/src/main/java/hello/itemservice/controller/ItemForm.java @@ -0,0 +1,13 @@ +package hello.itemservice.controller; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class ItemForm { + + private String itemName; + private int price; + private int quantity; +} diff --git a/4 WEEK/item-service/src/main/java/hello/itemservice/domain/Item.java b/4 WEEK/item-service/src/main/java/hello/itemservice/domain/Item.java new file mode 100644 index 00000000..8a0fd249 --- /dev/null +++ b/4 WEEK/item-service/src/main/java/hello/itemservice/domain/Item.java @@ -0,0 +1,22 @@ +package hello.itemservice.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class Item { + private Long itemId; + private String itemName; + private int price; + private int quantity; + + public Item(String itemName, int price, int quantity) { + this.itemName = itemName; + this.price = price; + this.quantity = quantity; + } +} diff --git a/4 WEEK/item-service/src/main/java/hello/itemservice/repository/ItemRepository.java b/4 WEEK/item-service/src/main/java/hello/itemservice/repository/ItemRepository.java new file mode 100644 index 00000000..172cafaa --- /dev/null +++ b/4 WEEK/item-service/src/main/java/hello/itemservice/repository/ItemRepository.java @@ -0,0 +1,16 @@ +package hello.itemservice.repository; + +import hello.itemservice.domain.Item; + +import java.util.List; + +public interface ItemRepository { + + void save(Item item); + + Item findByItemId(Long itemId); + + List findAll(); + + void editItem(Long itemId, Item editedItem); +} diff --git a/4 WEEK/item-service/src/main/java/hello/itemservice/repository/MemoryItemRepository.java b/4 WEEK/item-service/src/main/java/hello/itemservice/repository/MemoryItemRepository.java new file mode 100644 index 00000000..c26f7221 --- /dev/null +++ b/4 WEEK/item-service/src/main/java/hello/itemservice/repository/MemoryItemRepository.java @@ -0,0 +1,37 @@ +package hello.itemservice.repository; + +import hello.itemservice.domain.Item; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +@Repository +public class MemoryItemRepository implements ItemRepository{ + private static ConcurrentHashMap store = new ConcurrentHashMap<>(); + + private static long SEQUENCE = 0L; + + //상품 등록 (상품명, 가격, 수량) + public void save(Item item) { + item.setItemId(++SEQUENCE); + store.put(item.getItemId(), item); + } + + //상품 상세 + public Item findByItemId(Long itemId) { + return store.get(itemId); + } + + public List findAll() { + return new ArrayList<>(store.values()); + } + + //상품 수정 + public void editItem(Long itemId, Item editedItem) { + editedItem.setItemId(itemId); + store.put(editedItem.getItemId(), editedItem); + } + +} diff --git a/4 WEEK/item-service/src/main/java/hello/itemservice/service/ItemService.java b/4 WEEK/item-service/src/main/java/hello/itemservice/service/ItemService.java new file mode 100644 index 00000000..507845bb --- /dev/null +++ b/4 WEEK/item-service/src/main/java/hello/itemservice/service/ItemService.java @@ -0,0 +1,20 @@ +package hello.itemservice.service; + +import hello.itemservice.domain.Item; + +import java.util.List; + +public interface ItemService { + + //상품 목록 추가 + void addItem(Item item); + + //상품상세 + Item findItem(Long itemId); + + List findAll(); + + void editItem(Long itemId, Item item); +} + + diff --git a/4 WEEK/item-service/src/main/java/hello/itemservice/service/ItemServiceImpl.java b/4 WEEK/item-service/src/main/java/hello/itemservice/service/ItemServiceImpl.java new file mode 100644 index 00000000..de46f0e5 --- /dev/null +++ b/4 WEEK/item-service/src/main/java/hello/itemservice/service/ItemServiceImpl.java @@ -0,0 +1,36 @@ +package hello.itemservice.service; + +import hello.itemservice.domain.Item; +import hello.itemservice.repository.ItemRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ItemServiceImpl implements ItemService{ + + private final ItemRepository itemRepository; + + public ItemServiceImpl(ItemRepository itemRepository) { + this.itemRepository = itemRepository; + } + + //상품 목록 추가 + + public void addItem(Item item) { + itemRepository.save(item); + } + + //상품 상세 + public Item findItem(Long itemId) { + return itemRepository.findByItemId(itemId); + } + + public List findAll() { + return itemRepository.findAll(); + } + + public void editItem(Long itemId, Item item) { + itemRepository.editItem(itemId, item); + } +} diff --git a/4 WEEK/item-service/src/main/resources/application.properties b/4 WEEK/item-service/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/4 WEEK/item-service/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/4 WEEK/item-service/src/main/resources/templates/addItemForm.html b/4 WEEK/item-service/src/main/resources/templates/addItemForm.html new file mode 100644 index 00000000..0f13e4f8 --- /dev/null +++ b/4 WEEK/item-service/src/main/resources/templates/addItemForm.html @@ -0,0 +1,30 @@ + + + + + + + Document + + + +
+

상품 상세

+
+ + +
+ + +
+ + +
+ +
+
+ + \ No newline at end of file diff --git a/4 WEEK/item-service/src/main/resources/templates/editItemForm.html b/4 WEEK/item-service/src/main/resources/templates/editItemForm.html new file mode 100644 index 00000000..1c091052 --- /dev/null +++ b/4 WEEK/item-service/src/main/resources/templates/editItemForm.html @@ -0,0 +1,30 @@ + + + + + + + 상품 수정 + + +

상품 수정

+
+ + +
+ + +
+ + +
+ + +
+ +
+ + + + \ No newline at end of file diff --git a/4 WEEK/item-service/src/main/resources/templates/home.html b/4 WEEK/item-service/src/main/resources/templates/home.html new file mode 100644 index 00000000..7849f03c --- /dev/null +++ b/4 WEEK/item-service/src/main/resources/templates/home.html @@ -0,0 +1,38 @@ + + + + + + + Document + + + +
+

상품목록


+ + + + + + + + +
ID 상품명 가격 수량
+ + +
+ + \ No newline at end of file diff --git a/4 WEEK/item-service/src/main/resources/templates/itemDetailForm.html b/4 WEEK/item-service/src/main/resources/templates/itemDetailForm.html new file mode 100644 index 00000000..c4383940 --- /dev/null +++ b/4 WEEK/item-service/src/main/resources/templates/itemDetailForm.html @@ -0,0 +1,23 @@ + + + + + + + 상품 상세 + + + + + + + + + + +
ID 상품명 가격 수량
+ + + + \ No newline at end of file diff --git a/4 WEEK/item-service/src/test/java/hello/itemservice/ItemServiceApplicationTests.java b/4 WEEK/item-service/src/test/java/hello/itemservice/ItemServiceApplicationTests.java new file mode 100644 index 00000000..e2ded1be --- /dev/null +++ b/4 WEEK/item-service/src/test/java/hello/itemservice/ItemServiceApplicationTests.java @@ -0,0 +1,13 @@ +package hello.itemservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ItemServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/4 WEEK/item-service/src/test/java/hello/itemservice/service/ItemServiceTest.java b/4 WEEK/item-service/src/test/java/hello/itemservice/service/ItemServiceTest.java new file mode 100644 index 00000000..880a1c05 --- /dev/null +++ b/4 WEEK/item-service/src/test/java/hello/itemservice/service/ItemServiceTest.java @@ -0,0 +1,84 @@ +package hello.itemservice.service; + +import hello.itemservice.domain.Item; +import hello.itemservice.repository.ItemRepository; +import hello.itemservice.repository.MemoryItemRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class ItemServiceTest { + + ItemService itemService; + + + @BeforeEach + void beforeEach() { +// AppConfig appConfig = new AppConfig(); +// itemService = appConfig.itemService(); + itemService = new ItemServiceImpl(new MemoryItemRepository()); + + } + @Test + void add() { + //상품 등록 + //given + Item item = new Item("HTTP BOOK", 10000, 10); //상품명, 가격, 수량 + + //when + itemService.addItem(item); + Item findItem = itemService.findItem(1L); + + //then + assertThat(item).isEqualTo(findItem); + } + + + + @Test + void findItem() { + } + + + @Test + void findAll() { + //상품 등록 + //given + Item item1 = new Item("HTTP BOOK", 10000, 10); //상품명, 가격, 수량 + Item item2 = new Item("JPA BOOK", 43000, 5); //상품명, 가격, 수량 + Item item3 = new Item("Spring BOOK", 20000, 100); //상품명, 가격, 수량 + + //when + itemService.addItem(item1); + itemService.addItem(item2); + itemService.addItem(item3); + + //then + List allItems = itemService.findAll(); + assertThat(allItems).contains(item1, item2, item3); + } + + @Test + void editItem() { + //상품등록 + //given + Item item1 = new Item("HTTP BOOK", 10000, 10); //상품명, 가격, 수량 + itemService.addItem(item1); + + //when + Item editItem = new Item("JSP BOOK", 5000, 5); + itemService.editItem(1L, editItem); + + //then + Item editedItem = itemService.findItem(item1.getItemId()); // 수정 후 아이템 조회 + assertThat(editedItem.getItemId()).isEqualTo(1); + assertThat(editedItem.getItemName()).isEqualTo("JSP BOOK"); + assertThat(editedItem.getPrice()).isEqualTo(5000); + assertThat(editedItem.getQuantity()).isEqualTo(5); + + } +} \ No newline at end of file diff --git a/7 WEEK/seob/.gitignore b/7 WEEK/seob/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/7 WEEK/seob/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/7 WEEK/seob/7WEEK.md b/7 WEEK/seob/7WEEK.md new file mode 100644 index 00000000..ce8f3038 --- /dev/null +++ b/7 WEEK/seob/7WEEK.md @@ -0,0 +1,1323 @@ +# 8. 빈 생명주기 콜백 +___ +## 빈 생명주기 콜백 시작 + +데이터베이스 커넥션 풀, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, +애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화 작업이 필요. + +`NetworkClient` 는 애플리케이션 시작 시점에 `connect()`를 호출해서 연결, 애플리케이션이 종료되면 +`disConnect()`를 호출해서 연결을 끊어야 한다. + + +```java +package hello.core.lifecycle; + +import org.junit.jupiter.api.Test; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +public class BeanLifeCycleTest { + + @Test + public void lifeCycleTest() { + ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class); + NetworkClient client = ac.getBean(NetworkClient.class); + ac.close(); + } + + @Configuration + static class LifeCycleConfig { + @Bean + public NetworkClient networkClient() { + NetworkClient networkClient = new NetworkClient(); + networkClient.setUrl("http://hello-spring.dev"); + return networkClient; + } + } +} +``` + +```java +package hello.core.lifecycle; + +public class NetworkClient { + + private String url; + + public NetworkClient() { + System.out.println("생성자 호출, url = " + url); + connect(); + call("초기화 연결 메시지"); + } + + public void setUrl(String url) { + this.url = url; + } + + //서비스 시작시 호출 + public void connect() { + System.out.println("connect: " + url); + } + + public void call(String message) { + System.out.println("call: " + url + " message = " + message); + } + + //서비스 종료시 호출 + public void disconnect() { + System.out.println("close: " + url); + } +} +``` + +실행결과 +``` +생성자 호출, url = null +connect: null +call: null message = 초기화 연결 메시지 +``` + +@Bean 에서 NetworkClient 생성자 호출 -> setUrl 을 하지 않아 null 표시 -> connect 역시 null -> call message도 null 표시함
+생성자 호출 후 setter로 url 등록하였음. + +스프링 빈은 간단하게 다음과 같은 라이프 사이클을 가진다.
+**객체 생성** -> **의존관계 주입** + +스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에 필요한 데이터를 사용할 수 있는 준비가 완료된다. +따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.
+**스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공**한다. +또한 **스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백**을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다. + +**스프링 빈의 이벤트 라이프사이클**
+**스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료** +- **초기화 콜백** : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출 +- **소멸전 콜백** : 빈이 소멸되기 직전에 호출 + +>**참고 : 객체의 생성과 초기화를 분리하자**
+>생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 반면에 초기화는 +> 어떻게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다.
+> 따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확학게 +> 나누는 것이 유지보수 관점에서 좋다. 물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 +> 한번에 다 처리하는게 더 나을 수 있다. + +


+ +**스프링이 지원하는 빈 생명주기 콜백** +- 인터페이스(InitializeBean, DisposableBean) +- 설정 정보에 초기화 메서드, 종료 메서드 지정 +- @PostConstruct, @PreDestroy 애노테이션 지원 + +

+ +### 인터페이스(InitializeBean, DisposableBean) +```java +package week7.seob.lifecycle; + + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; + +public class NetworkClient implements InitializingBean, DisposableBean { + + private String url; + + public NetworkClient() { + System.out.println("생성자 호출, url = " + url); + } + + public void setUrl(String url) { + this.url = url; + } + + //서비스 시작시 호출 + public void connect() { + System.out.println("connect: " + url); + } + + public void call(String message) { + System.out.println("call: " + url + " message = " + message); + } + + //서비스 종료시 호출 + public void disconnect() { + System.out.println("close: " + url); + } + + //의존관계 주입이 끝난 후 + @Override + public void afterPropertiesSet() throws Exception { + System.out.println("\nNetworkClient.afterPropertiesSet"); + connect(); + call("초기화 연결 메시지"); + } + + //Bean이 종료될 때 + @Override + public void destroy() throws Exception { + System.out.println("\nNetworkClient.destory"); + disconnect(); + } +} +``` + +실행결과 +``` +생성자 호출, url = null + +NetworkClient.afterPropertiesSet +connect: http://hello-spring.dev +call: http://hello-spring.dev message = 초기화 연결 메시지 + +NetworkClient.destory +close: http://hello-spring.dev +``` + +**초기화, 소멸 인터페이스 단점** +- 이 인터페이스는 스프링 전용 인터페이스. 해당 코드가 스프링 전용 인터페이스에 의존한다. +- 초기화, 소멸 메서드의 이름을 변경할 수 없다. +- 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다. + +>참고 : 인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이고, 지금은 더 나은 방법들이 있어서 거의 사용하지 않는다. + + +

+### 빈 등록 초기화, 소멸 메서드 + +설정 정보에 `@Bean(initMethod = "init", destroyMethod = "close"` 처럼 초기화, 소멸 메서드 지정 + +```java +package week7.seob.lifecycle; + + +public class NetworkClient { + + private String url; + + public NetworkClient() { + System.out.println("생성자 호출, url = " + url); + } + + public void setUrl(String url) { + this.url = url; + } + + //서비스 시작시 호출 + public void connect() { + System.out.println("connect: " + url); + } + + public void call(String message) { + System.out.println("call: " + url + " message = " + message); + } + + //서비스 종료시 호출 + public void disconnect() { + System.out.println("close: " + url); + } + + //의존관계 주입이 끝난 후 + public void init() { + System.out.println("\nNetworkClient.init"); + connect(); + call("초기화 연결 메시지"); + } + + //Bean이 종료될 때 + public void close() { + System.out.println("\nNetworkClient.close"); + disconnect(); + } +} +``` + + +```java +package week7.seob.lifecycle; + +import org.junit.jupiter.api.Test; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +public class BeanLifeCycleTest { + + + @Test + public void lifeCycleTest() { + ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class); + NetworkClient client = ac.getBean(NetworkClient.class); + ac.close(); + } + + + @Configuration + static class LifeCycleConfig { + @Bean(initMethod = "init", destroyMethod = "close") + public NetworkClient networkClient() { + NetworkClient networkClient = new NetworkClient(); + networkClient.setUrl("http://hello-spring.dev"); + return networkClient; + } + } +} +``` +실행결과 +``` +생성자 호출, url = null + +NetworkClient.init +connect: http://hello-spring.dev +call: http://hello-spring.dev message = 초기화 연결 메시지 + +NetworkClient.close +close: http://hello-spring.dev +``` + +**설정 정보 사용 특징** +- 메서드 이름을 자유롭게 줄 수 있다. +- 스프링 빈이 스프링 코드에 의존하지 않는다. +- 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다. + +**종료 메서드 추론** +- `@Bean의 destroyMethod` 속성에는 아주 특별한 기능이 있다. +- 라이브러리는 대부분 `close`, `shutdown` 이라는 이름의 종료 메서드를 사용한다. +- @Bean의 `destroyMethod`는 기본값이 `(inferred)`(추론) 으로 등록되어 있다. +- 이 추론 기능은 `close`, `shutdown`라는 이름의 메서드를 자동으로 호출해준다. 이름 그대로 종료 메서드를 추론해서 호출해준다. +- 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다. +- 추론 기능을 사용하기 싫으면 `destroyMethod=""`처럼 빈 공백을 지정하면 된다. + +

+ +### 애노테이션 @PostConstruct, @PreDestroy +```java +package week7.seob.lifecycle; + + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +public class NetworkClient { + + private String url; + + public NetworkClient() { + System.out.println("생성자 호출, url = " + url); + } + + public void setUrl(String url) { + this.url = url; + } + + //서비스 시작시 호출 + public void connect() { + System.out.println("connect: " + url); + } + + public void call(String message) { + System.out.println("call: " + url + " message = " + message); + } + + //서비스 종료시 호출 + public void disconnect() { + System.out.println("close: " + url); + } + + //의존관계 주입이 끝난 후 + @PostConstruct + public void init() { + System.out.println("\nNetworkClient.init"); + connect(); + call("초기화 연결 메시지"); + } + + //Bean이 종료될 때 + @PreDestroy + public void close() { + System.out.println("\nNetworkClient.close"); + disconnect(); + } +} +``` + +```java +package week7.seob.lifecycle; + +import org.junit.jupiter.api.Test; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +public class BeanLifeCycleTest { + + + @Test + public void lifeCycleTest() { + ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class); + NetworkClient client = ac.getBean(NetworkClient.class); + ac.close(); + } + + + @Configuration + static class LifeCycleConfig { + @Bean //(initMethod = "init", destroyMethod = "close") + public NetworkClient networkClient() { + NetworkClient networkClient = new NetworkClient(); + networkClient.setUrl("http://hello-spring.dev"); + return networkClient; + } + } +} +``` + +실행결과 +``` +생성자 호출, url = null + +NetworkClient.init +connect: http://hello-spring.dev +call: http://hello-spring.dev message = 초기화 연결 메시지 + +NetworkClient.close +close: http://hello-spring.dev +``` + +`@PostConstruct`, `@PreDestroy`이 두 애노페이션을 사용하면 가장 편리하게 초기화와 종료를 실행할 수 있다. + + +**@PostConstruct, @PreDestroy 애노테이션 특징** +- 최신 스프링에서 가장 권장하는 방법. +- 애노테이션 하나만 붙이면 되므로 매우 편리. +- 패키지가 `javax.annotation.PostConstruct`이다. 스프리에 종속적인 기술이 아니라 JSR-250라는 자바 표준이다. +따라서 스프링이 아닌 다른 컨테이너에서도 동작한다. +- 컴포넌트 스캔과 잘 어울린다. +- 유일한 단점 : 외부 라이브러리에 적용하지 못함. 외부 라이브러리 초기화, 종료를 해야 한다면 `@Bean` 의 기능을 사용해야한다. + +**정리** +- **@PostConstruct, @PreDestroy 애노테이션을 사용하자** +- 코드를 고칠 수 없는 외부 라이브러리 초기화, 종료를 해야한다면 `@Bean` 의 `initMethod`, `destroyMethod`를 사용하자. + + + +

+

+ +___ +

+ +# 9. 빈 스코프 +___ + +## 빈 스코프란 +스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때 까지 유지된다고 학습함. +이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다. + +**스프링은 다음과 같은 다양한 스코프를 지원한다.** +- **싱글톤** : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프. +- **프로토타입** : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프. +- **웹 관련 스코프** : + - **request** : 웹 요청이 들어오고 나갈때 까지 유지되는 스코프. + - **session** : 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프. + - **application** : 웹의 Servlet Context와 같은 범위로 유지되는 스코프이다. + + +
+빈 스코프 지정 방법

+ +**컴포넌트 스캔 자동 등록** +```java +@Scope("prototype") +@Component +public static HelloBean{} +``` + + +**수동 등록** +```java +@Scope("prototype") +@Bean +PrototypeBean HelloBean(){ + return new HelloBean(); +} +``` + +```java +package week7.seob.scope; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Scope; + +public class SingletonTest { + + @Test + void singletonBeanFind() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class); + SingletonBean singletonBean1 = ac.getBean(SingletonBean.class); + SingletonBean singletonBean2 = ac.getBean(SingletonBean.class); + + System.out.println("singletonBean1 = " + singletonBean1); + System.out.println("singletonBean2 = " + singletonBean2); + Assertions.assertThat(singletonBean1).isEqualTo(singletonBean2); + + ac.close(); + } + + @Scope("singleton") + static class SingletonBean{ + @PostConstruct + public void init() { + System.out.println("SingletonBean.init"); + } + + @PreDestroy + public void destroy() { + System.out.println("SingletonBean.destroy"); + } + } + +} +``` +실행결과 +``` +SingletonBean.init +singletonBean1 = week7.seob.scope.SingletonTest$SingletonBean@773f7880 +singletonBean2 = week7.seob.scope.SingletonTest$SingletonBean@773f7880 +SingletonBean.destroy +``` +싱글톤 스코프인 경우 같은 인스턴스를 반환하는것을 확인할 수 있다. + +## 프로토타입 스코프 +**싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환.** 반면에 +**프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 다른 인스턴스를 생성해서 반환** + +**싱글톤 빈 요청** +1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청 +2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환 +3. 이후에 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈을 반환한다. + +**프로토타입 빈 요청1** +1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청. +2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성, 필요한 의존관계를 주입. + +**프로토타입 빈 요청2** +3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환.(관리는 X) +4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환 + +**정리**
+**핵심은 스프링 컨테이너는 프로토타입 빈을 생성, 의존관계 주입, 초기화 까지만 처리** 클라이언트에 빈을 반환하고, +이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다. 프로토타입 빈을 관리할 책임은 프로토타입 빈을 +받은 클라이언트에 있다. **그래서 `@PreDestroy`같은 종료 메서드가 호출되지 않는다.** + + + +```java +package week7.seob.scope; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Scope; + +public class PrototypeTest { + + @Test + void prototypeBeanFind() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class); + System.out.println("find prototypeBean1"); + PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); + System.out.println("find prototypeBean2"); + PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); + + System.out.println("prototypeBean1 = " + prototypeBean1); + System.out.println("prototypeBean2 = " + prototypeBean2); + Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2); + + ac.close(); + } + + @Scope("prototype") + static class PrototypeBean { + @PostConstruct + public void init() { + System.out.println("PrototypeBean.init"); + } + + @PreDestroy + public void destroy() { + System.out.println("PrototypeBean.destroy"); + } + } +} +``` +실행결과 +``` +find prototypeBean1 +PrototypeBean.init +find prototypeBean2 +PrototypeBean.init +prototypeBean1 = week7.seob.scope.PrototypeTest$PrototypeBean@773f7880 +prototypeBean2 = week7.seob.scope.PrototypeTest$PrototypeBean@878452d +``` + +- 싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행되지만, 프로토타입 스코프의 빈은 스프링 컨테이너에서 +빈을 조회할 때 생성되고, 초기화 메서드도 실행된다. +- 프로타입 빈을 2번 조회했으므로 완전히 다른 스프링 빈이 생성되고, 초기화도 2번 실행된 것을 확인할 수 있다. +- 싱글톤 빈은 스프링 컨테이너가 관리하기 때문에 스프링 컨테이너가 종료될 때 빈의 종료 메서드가 실행되지만, +**프로토타입 빈은 스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화 까지만 관여하고, 더는 관리하지 않는다. +따라서 프로토타입 빈은 스프링 컨테이너가 종료될 때 `@PreDestroy`같은 종료 메서드가 전혀 실행되지 않는다.** + + +**프로토타입 빈의 특징 정리** +- 스프링 컨테이너에 요청할 떄 마다 새로 생성 +- 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입, 초기화까지만 관여 +- 종료 메서드가 호출되지 않음 +- 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다. + +```java +prototypeBean1.destoroy(); +prototypeBean2.destoroy(); +``` +위와 같이 직접 destroy()를 호출하여 직접 종료할 수 있음. + + + +## 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제 +스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환
+하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야함 + +### 프르토타입 빈 직접 요청 +```java +package week7.seob.scope; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Scope; + +public class SingletonWithPrototypeTest1 { + + @Test + void prototypeFind() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class); + + PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); + prototypeBean1.addCount(); + + PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); + prototypeBean2.addCount(); + + Assertions.assertThat(prototypeBean1.getCount()).isEqualTo(1); + Assertions.assertThat(prototypeBean2.getCount()).isEqualTo(1); + } + + + @Scope("prototype") + static class PrototypeBean { + private int count = 0; + + public void addCount() { + count++; + } + + public int getCount() { + return count; + } + + @PostConstruct + public void init() { + System.out.println("PrototypeBean.init " + this); + } + + @PreDestroy + public void destroy() {//호출 X + System.out.println("PrototypeBean.destory "+ this); + } + } +} +``` +실행결과 +``` +PrototypeBean.init week7.seob.scope.SingletonWithPrototypeTest1$PrototypeBean@51e8e6e6 +PrototypeBean.init week7.seob.scope.SingletonWithPrototypeTest1$PrototypeBean@878452d +``` + +
+스프링 컨테이너에 프로토타입 빈 직접 요청 1. +1. prototypeBean1는 스프링 컨테이너에 스프링 빈을 요청 +2. 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(**PrototypeBean@51e8e6e6**)한다. 해당 빈의 count 값은 0. +3. prototypeBean1는 조회한 프로토타입 빈에 `addCount()` 호출 -> count 필드가 1 증가. +
+스프링 컨테이너에 프로토타입 빈 직접 요청 2. +1. prototypeBean1는 스프링 컨테이너에 스프링 빈을 요청 +2. 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(**PrototypeBean@51e8e6e6**)한다. 해당 빈의 count 값은 0. +3. prototypeBean1는 조회한 프로토타입 빈에 `addCount()` 호출 -> count 필드가 1 증가. + +**결론** +- 프로토타입 빈은 요청시 항상 새로운 빈을 생성하기 때문에 서로 다른 필드라 생각하면 됨 + +### 싱글톤 빈에서 프로토타입 빈 사용 + +**싱글톤에서 프로토타입 빈 사용1** +- `clientBean`은 싱글톤임. 스프링 컨테이너 생성 시점에 함께 생성, 의존관계 주입도 발생 +- 1. `clientBean`은 의존관계 자동 주입 사용. 주입 시점에 스프링 컨테이너에 프로토타입 빈 요청 +- 2. 스프링 컨테이너는 프로토타입 빈을 생성해서 `clientBean`에 반환. -> 프로토타입 빈의 count 필드 값 = 0 +- `clientBean`은 프로토타입 빈을 내부 필드에 보관(참조값 보관) + +**싱글톤에서 프로토타입 빈 사용2** +- 클라이언트A가 `clientBean`을 스프링 컨테이너에 요청. 싱글톤이므로 항상 같은 `clientBean` 반환 +- 3. 클라이언트A는 `clientBean.logic()`호출 +- 4. `clientBean`은 prototypeBean의 `addCount()`호출해서 프로토타입 빈의 count를 증가시킴. -> count = 1 + +**싱글톤에서 프로토타입 빈 사용3** +- 클라이언트A가 `clientBean`을 스프링 컨테이너에 요청. 싱글톤이므로 항상 같은 `clientBean` 반환 +- **clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈. 주입 시점에 스프링 컨테이너에 +요청해서 프로토타입이 새로 생성된 것이지, 사용할 때마다 생성되는것이 아님.** +- 3. 클라이언트A는 `clientBean.logic()`호출 +- 4. `clientBean`은 prototypeBean의 `addCount()`호출해서 프로토타입 빈의 count를 증가시킴. -> count = 2 + +코드를 확인해보자 +```java +package week7.seob.scope; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Scope; + +public class SingletonWithPrototypeTest1 { + + @Test + void prototypeFind() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class); + + PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); + prototypeBean1.addCount(); + Assertions.assertThat(prototypeBean1.getCount()).isEqualTo(1); + + PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); + prototypeBean2.addCount(); + Assertions.assertThat(prototypeBean2.getCount()).isEqualTo(1); + } + + @Test + void singletonClientUsePrototype() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class); + + ClientBean clientBean1 = ac.getBean(ClientBean.class); + int count1 = clientBean1.logic(); + Assertions.assertThat(count1).isEqualTo(1); + + ClientBean clientBean2 = ac.getBean(ClientBean.class); + int count2 = clientBean2.logic(); + Assertions.assertThat(count2).isEqualTo(2); + + } + + @Scope("singleton") + static class ClientBean{ + private final PrototypeBean prototypeBean; //생성 시점에 주입 + + @Autowired + public ClientBean(PrototypeBean prototypeBean) { + this.prototypeBean = prototypeBean; + } + + public int logic() { + prototypeBean.addCount(); + int count = prototypeBean.getCount(); + return count; + } + } + @Scope("prototype") + static class PrototypeBean { + private int count = 0; + + public void addCount() { + count++; + } + + public int getCount() { + return count; + } + + @PostConstruct + public void init() { + System.out.println("PrototypeBean.init " + this); + } + + @PreDestroy + public void destroy() {//호출 X + System.out.println("PrototypeBean.destory "+ this); + } + } +} +``` +실행결과 +``` +PrototypeBean.init week7.seob.scope.SingletonWithPrototypeTest1$PrototypeBean@25748410 +``` + +스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다. +그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에 프로토타입 빈이 새로 생성되기는 하지만, +싱글톤 빈과 함꼐 계속 유지되는것이 문제이다 + +> **참고** : 여러번 빈에서 같은 프로토타입 빈을 주입 받으면, **주입 받는 시점에 각각 새로운 프로토타입 빈이 생성**
+> clientA. clientB가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입 받는다.
+> clientA -> prototypeBean@x01 +> clientB -> prototypeBean@x02
+> 물론 사용할 때 마다 새로 생성되는 것은 아니다. + + + + +## 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결 +싱글톤 빈과 프로토타입 빈을 함께 사용할때 Provider를 사용하면 항상 새로운 프로토타입 빈을 생성할 수 있다. + + +가장 간단한 방법은 싱글톤 빈이 프로토타입 빈을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다. +```java + @Scope("singleton") + static class ClientBean{ + + @Autowired + private ApplicationContext ac; + + public int logic() { + PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); + prototypeBean.addCount(); + int count = prototypeBean.getCount(); + return count; + } + } +``` +실행결과 +``` +PrototypeBean.init week7.seob.scope.SingletonWithPrototypeTest1$PrototypeBean@77602954 +PrototypeBean.init week7.seob.scope.SingletonWithPrototypeTest1$PrototypeBean@6fff253c +``` + +- `ac.getBean()`을 통해 항상 새로운 프로토타입 빈을 불러오는 것을 확인할 수 있다. +- 의존관계를 외부에서 주입(DI)받는게 아니라 위처럼 직접 필요한 의존관계를 찾는 것을 +**Dependency Lookup(DL)** 의존관계 조회(탐색) 이라고 한다. +- 그런데 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트가 어려워진다. +- 현재 필요한 기능 -> 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL 정도의 기능만 제공하는 무언가가 있으면 된다. + + +### ObjectFactory, ObjectProvider +지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 `ObjectProvider`이다.
+과거에는 `ObjectFactory`가 존재했는데, 여기에 편의 기능을 추가한 `ObjectProvider`가 만들어졌다. + +```java + @Scope("singleton") + static class ClientBean{ + + @Autowired + private ObjectProvider prototypeBeanProvider; + + public int logic() { + PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); + prototypeBean.addCount(); + int count = prototypeBean.getCount(); + return count; + } + } +``` +실행결과 +```java +PrototypeBean.init week7.seob.scope.SingletonWithPrototypeTest1$PrototypeBean@10fde30a +PrototypeBean.init week7.seob.scope.SingletonWithPrototypeTest1$PrototypeBean@72f46e16 +``` +- `prototypeBeanProvider.getObject()`를 통해서 항상 새로운 프로토타입 빈이 생성된다. +- `ObjectProvider`의 `getObject()`를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아 반환한다.(**DL**) +- 기능이 단순하므로 단위테스트 및 mock 코드를 만들기 훨씬 쉽다.
+mock : 실제 객체를 만들기에는 비용과 시간이 많이 들거나 의존성이 크게 걸쳐져 있어서 테스트 시 제대로 구현하기 어려울 경우 가짜 객체를 만들어서 사용하는 기술. +- `ObjectProvider`는 지금 딱 필요한 DL정도의 기능만 제공한다. + +**특징** +- ObjectFactory : 기능이 단순,별도의 라이브러리 필요 없음, 스프링에 의존 +- ObjectProvider : ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존 + + +### JSR-330 Provider +`javax.inject.Provider`라는 JSR-300 자바 표준을 사용하는 방법이 있다. **스프링에 의존하지 않는다** +
이 방법을 사용하려면 `javax.inject:javax.inject:1` 라이브러리를 gradle에 추가해야 한다. + + +```java +//build.gradle에 추가 -> implementation 'javax.inject:javax.inject:1' +@Scope("singleton") + static class ClientBean{ + + @Autowired + private Provider prototypeBeanProvider; + + public int logic() { + PrototypeBean prototypeBean = prototypeBeanProvider.get(); + prototypeBean.addCount(); + int count = prototypeBean.getCount(); + return count; + } + } +``` +실행결과 +``` +PrototypeBean.init week7.seob.scope.SingletonWithPrototypeTest1$PrototypeBean@7b64240d +PrototypeBean.init week7.seob.scope.SingletonWithPrototypeTest1$PrototypeBean@46e8a539 +``` + +- 실행해보면 `provider.get()`을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다. +- `provider`의 `get()`을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다(DL) +- 자바표준, 기능이 단순해 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다 +- `provider`는 현재 필요한 DL정도의 기능을 제공한다 + +**특징** +- `get()` 메서드 하나로 기능이 매우 단순하다. +- 별도의 라이브러리가 필요하다. +- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다. + + +**정리** +- 프로토타입 빈은 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다. +하지만 실무에선 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일이 매우 드물다고 한다 +- `ObjectProvider`, `JSR330 Provider`등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다. + +> **참고** : 스프링이 제공하는 메서드에 `@Lookup`애노테이션을 사용하는 방법도 있지만, 이전 방법들로 충분하고, +고려해야할 내용도 많다. + + +> **참고** : ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별도의 의존관계 추가가 +> 필요 없기 때문에 편리하다. +> +> 스프링을 사용하다보면 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠 때가 있다. 대부분 스프링이 더 다양하고 +> 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 쓰자! + + + + +## 웹 스코프 +싱글톤은 스프링 컨테이너의 시작과 끝까지 함꼐하는 매우 긴 스코프, 프로토타입은 생성과 의존관계 주입, 그리고 초기화 까지만 진행하는 특별한 스코프 + +**웹 스코프의 특징** +- 웝 스코프는 웹 환경에서만 동작 +- 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리. 따라서 종료 메서드 호출 + +**웹 스코프 종류** +- **request** : HTTP요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP요청마다 별도의 빈 인스턴트가 생성, 관리된다. +- **session** : HTTP Session과 동일한 생명주기를 가지는 스코프 +- **application** : 서블릿 컨텍스트(`ServletContext`) 와 동일한 생명주기를 갖는 스코프 +- **websocket** : 웹 소켓과 동일한 생명주기를 가지는 스코프 + + +## request 스코프 예제 + +### 웹 환경 추가 +웹 스코프는 웹 환경에서만 동작함. 따라서 web환경이 동작하도록 라이브러리를 추가한다. +```groovy + implementation 'org.springframework.boot:spring-boot-starter-web' +``` +> **참고** : `spring-boot-starter-web` 라이브러리를 추가하면 스프링 부트는 내장 톰켓 서버를 활용해 웹 서버와 스프링을 함께 실행시킨다. + +> **참고** : 스프링 부트는 웹 라이브러리가 없으면 `AnnotationConfigApplicationContext`를 기반으로 +> 웹 애플리케이션을 구동한다. 웹 라이브러리가 추가되면 웹과 관련된 추카 설정과 환경들이 필요하므로 +> `AnnotationConfigServletWebServerApplicationContext`를 기반으로 애플리케이션을 구동한다. + +### request 스코프 예제 개발 +동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.
+이럴 때 사용하기 좋은 것이 request 스코프이다. + +```java +package week7.seob.common; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +@Scope(value = "request") +public class MyLogger { + + private String uuid; + private String requestURL; + + public void setRequestURL(String requestURL) { + this.requestURL = requestURL; + } + + public void log(String message) { + System.out.println("[" + uuid + "][" + requestURL + "]" + message); + } + + @PostConstruct + public void init() { + uuid = UUID.randomUUID().toString(); + System.out.println("[" + uuid + "] request scope bean create : " + this); + } + + @PreDestroy + public void close() { + System.out.println(""); + System.out.println("[" + uuid + "] request scope bean close : " + this); + } +} +``` +- 로그를 출력하기 위한 `MyLogger` 클래스 +- `@Scope(value="request)"`를 사용해 request 스코프를 지정. 이 빈은 HTTP 요청 당 하나씩 생성되고, +요청이 끝나는 시점에 소멸된다. +- 이 빈이 생성되는 시점에 자동으로 `@PostConstruct`초기화 메서드를 사용해 uuid를 생성 및 저장한다. +이 빈은 HTTP 요청 당 하나씩 생성되기 때문에 uud를 저장해두면 다른 HTTP요청과 구분이 가능하다. +- 이 빈이 소멸되는 시점에 `@PreDestroy`를 사용해 종료 매시지를 남긴다. +- `requestURL`은 이 빈이 생성되는 시점에는 알 수 없어 외부에서 setter로 입력받는다. + + +*MyLogger* +```java +package week7.seob.common; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +@Scope(value = "request") +public class MyLogger { + + private String uuid; + private String requestURL; + + public void setRequestURL(String requestURL) { + this.requestURL = requestURL; + } + + public void log(String message) { + System.out.println("[" + uuid + "][" + requestURL + "]" + message); + } + + @PostConstruct + public void init() { + uuid = UUID.randomUUID().toString(); + System.out.println("[" + uuid + "] request scope bean create : " + this); + } + + @PreDestroy + public void close() { + System.out.println(""); + System.out.println("[" + uuid + "] request scope bean close : " + this); + } +} +``` +- 로그를 출력하기 위한 클래스. +- `@Scope(value = "request")`로 requset 스코프 지정. 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸 +- 이 빈이 생성되는 시점에 자동으로 `@PostConstruct` 초기화 메서드를 사용해 uuid를 생성해서 저장. +이 빈은 HTTP 요총 당 하나씩 생성되므로 uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있다. +- 이 빈이 소멸되는 시점에 `@PreDestroy`를 사앵해서 종료 메시지를 남긴다. +- `requestURL`은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력받는다. + + +*LogDemoController* +```java +package week7.seob.web; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import week7.seob.common.MyLogger; + +@Controller +public class LogDemoController { + private final LogDemoService logDemoService; + private final MyLogger myLogger; + + @Autowired + public LogDemoController(LogDemoService logDemoService, MyLogger myLogger) { + this.logDemoService = logDemoService; + this.myLogger = myLogger; + } + + @RequestMapping("log-demo") + @ResponseBody + public String logDemo(HttpServletRequest request) { + String requestURL = request.getRequestURL().toString(); + myLogger.setRequestURL(requestURL); + + myLogger.log("controller test"); + logDemoService.logic("testId"); + return "OK"; + } + +} +``` +- 로거가 잘 작동하는 확인하는 테스트용 컨트롤러 +- 여기서 HttpServletRequest를 통해서 요청 URL을 받았다. + - requestURL 값 `http://localhost:8080/log-demo` +- 이렇게 받은 requestURL 값을 myLogger에 저장한다. myLogger는 HTTP 요청 당 각각 구분되므로 +다른 HTTP요청 때문에 값이 섞이는 걱정은 하지 않아도 된다. +- 컨트롤러에서 controller test라는 로그를 남긴다. + + +*LogDemoService* +```java +package week7.seob.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import week7.seob.common.MyLogger; + +@Service +public class LogDemoService { + + private final MyLogger myLogger; + + @Autowired + public LogDemoService(MyLogger myLogger) { + this.myLogger = myLogger; + } + + public void logic(String id) { + myLogger.log("service id = " + id); + } +} +``` +- request scope를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면, 파라미터가 많아 +지저분해진다. 더 문제는 requestURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게 된다. +웹과 관련된 부분은 컨트롤러 까지만 사용해야 한다. 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 +유지보수 과점에서 좋다. +- request scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고, MyLogger의 멤버변수에 저장해서 +코드와 계층을 깔끔하게 유지할 수 있다. + +실행 시 +``` +Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton +``` + +오류가 발생한다. 왜냐하면 request 가 와야 myLogger를 사용할 수 있지만 아직 request가 오지도 않은 상태에서 스프링이 myLogger를 요청했기 때문이다. +
+스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않는다. +이 빈은 실제 고객의 요청이 와야 생성할 수 있다. + + +## 스코프와 Provider +위의 문제의 첫 번째 해결방법은 Provider를 사용하는 것 이다. + +*LogDemoController* +```java +package week7.seob.web; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import week7.seob.common.MyLogger; + +@Controller +public class LogDemoController { + private final LogDemoService logDemoService; + private final ObjectProvider myLoggerProvider; + + @Autowired + public LogDemoController(LogDemoService logDemoService, ObjectProvider myLogger) { + this.logDemoService = logDemoService; + this.myLoggerProvider = myLogger; + } + + @RequestMapping("log-demo") + @ResponseBody + public String logDemo(HttpServletRequest request) { + String requestURL = request.getRequestURL().toString(); + MyLogger myLogger = myLoggerProvider.getObject(); + myLogger.setRequestURL(requestURL); + + myLogger.log("controller test"); + logDemoService.logic("testId"); + return "OK"; + } + +} +``` + +*LogDemoService* +```java +package week7.seob.web; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import week7.seob.common.MyLogger; + +@Service +public class LogDemoService { + + private final ObjectProvider myLoggerProvider; + + @Autowired + public LogDemoService(ObjectProvider myLogger) { + this.myLoggerProvider = myLogger; + } + + public void logic(String id) { + MyLogger myLogger = myLoggerProvider.getObject(); + myLogger.log("service id = " + id); + } +} + +``` + + +실행결과 (두 번 요청) +``` +[ae14a061-798d-4f36-a0a4-12e304ed743f] request scope bean create : week7.seob.common.MyLogger@10437b22 +[ae14a061-798d-4f36-a0a4-12e304ed743f][http://localhost:8080/log-demo]controller test +[ae14a061-798d-4f36-a0a4-12e304ed743f][http://localhost:8080/log-demo]service id = testId +[ae14a061-798d-4f36-a0a4-12e304ed743f] request scope bean close : week7.seob.common.MyLogger@10437b22 + +[ba090d99-d6c4-4708-8fac-a74c01dd6efe] request scope bean create : week7.seob.common.MyLogger@5f0bf03 +[ba090d99-d6c4-4708-8fac-a74c01dd6efe][http://localhost:8080/log-demo]controller test +[ba090d99-d6c4-4708-8fac-a74c01dd6efe][http://localhost:8080/log-demo]service id = testId +[ba090d99-d6c4-4708-8fac-a74c01dd6efe] request scope bean close : week7.seob.common.MyLogger@5f0bf03 +``` + +- `@ObjectProvider` 덕분에 `ObjectProvider.getObject()`를 호출하는 시점까지 request scope +**빈의 생성을 지연**할 수 있다. +- `ObjectProvider.getObject()`를 호출하는 시점에는 HTTP요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다. +- `ObjectProvider.getObject()`를 `LogDemoController`, `LogDemoService`에서 각각 한 번씩 따로 호출해도 +같은 HTTP 요청이면 같은 스프링이 반환된다. + + + +## Scope & Proxy +```java +@Component +@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) +public class MyLogger { +} +``` +- `proxyMode = ScopedProxyMode.TARGET_CLASS` 추가. + - 적용 대상이 클래스 일 때 `TARGET_CLASS` + - 적용 대상이 인터페이스 일 때 `INTERFACE` +- 이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 주입해 둘 수 있다. + +코드를 Provider 사용 이전으로 변경한다. +```java +@Controller +public class LogDemoController { + private final LogDemoService logDemoService; + private final MyLogger myLogger; + + @Autowired + public LogDemoController(LogDemoService logDemoService, MyLogger myLogger) { + this.logDemoService = logDemoService; + this.myLogger = myLogger; + } + + @RequestMapping("log-demo") + @ResponseBody + public String logDemo(HttpServletRequest request) { + String requestURL = request.getRequestURL().toString(); + myLogger.setRequestURL(requestURL); + + myLogger.log("controller test"); + logDemoService.logic("testId"); + return "OK"; + } + +} +``` + +```java +@Service +public class LogDemoService { + + private final MyLogger myLogger; + + @Autowired + public LogDemoService(MyLogger myLogger) { + this.myLogger = myLogger; + } + + public void logic(String id) { + myLogger.log("service id = " + id); + } +} +``` +실행결과 +```java +[bbb44759-be28-4fa0-8307-a8a983c9d824] request scope bean create : week7.seob.common.MyLogger@21cd3a44 +[bbb44759-be28-4fa0-8307-a8a983c9d824][http://localhost:8080/log-demo]controller test +[bbb44759-be28-4fa0-8307-a8a983c9d824][http://localhost:8080/log-demo]service id = testId +[bbb44759-be28-4fa0-8307-a8a983c9d824] request scope bean close : week7.seob.common.MyLogger@21cd3a44 + +[99a82c02-0a83-4ada-933f-a31d238811fc] request scope bean create : week7.seob.common.MyLogger@6a45bcb6 +[99a82c02-0a83-4ada-933f-a31d238811fc][http://localhost:8080/log-demo]controller test +[99a82c02-0a83-4ada-933f-a31d238811fc][http://localhost:8080/log-demo]service id = testId +[99a82c02-0a83-4ada-933f-a31d238811fc] request scope bean close : week7.seob.common.MyLogger@6a45bcb6 +``` + +### 웹 스코프 & 프록시 동작 원리 + +```java +System.out.println("myLogger = " + myLogger.getClass()); +``` +실행결과 +```java +myLogger = class week7.seob.common.MyLogger$$SpringCGLIB$$0 +``` + +**CGLIB 라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 개체를 만들어 주입한다.** +- `@Scope`의 `proxyMode` 를 설정하면 스프링 컨테이너는 CGLIB라는 바이트 코드를 조작하는 라이브러리를 사용해 +MyLogger를 상속받은 가짜 프록시 객체를 생성한다. +- 결과를 확인해보면 우리가 등록한 순수한 MyLogger클래스가 아니라 `MyLogger$$SpringCGLIB$$0`이라는 클래스로 만들어진 +객체가 대신 등록된 것을 확인할 수 있다. +- 그리고 스프링 컨테이너에 "myLogger"라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한다. +- `ac.getBean("MyLogger", MyLogger.class)`로 조회해도 프록세 객체가 조회된다. +- 따라서 의존관계 주입도 이 가짜 프록시 객체가 주입된다. + + +**가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.** +- 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다. +- 클라이언트가 `myLogger.logic()`을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다. +- 가짜 프록시 객체는 request스코프의 진짜 `myLogger.logic()`를 호출한다. +- 가짜 프록시 객체는 원본 클래스를 상속 받아 생성됐기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 +원본인지 아닌지도 모르게, 동일하게 사용할 수 있다.(다형성) + + + +> **동작 정리** +> - CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어 주입 +> - 이 가짜 프록시 객체는 실제 요청이 들어오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있음. +> - 가짜 프록시 객체는 실제 request scope와 관계 없음. 단순히 가짜이고, 내부에 위임 로직만 존재, 싱글톤 처럼 동작. + + +**특징 정리** +- 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다. +- Provider를 사용하든, proxy를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다. +- 단지 애노테이션 설정 변경만으로 원본 객체를 proxy 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 장점이다. +- 또한 웹 스코프가 아니어도 프록시는 사용할 수 있다. + + +**주의점** +- 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 주의해서 사용해야함 +- 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용, 무분별하게 사용시 유지보수하기 어려움 \ No newline at end of file diff --git a/7 WEEK/seob/build.gradle b/7 WEEK/seob/build.gradle new file mode 100644 index 00000000..0459288f --- /dev/null +++ b/7 WEEK/seob/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.1' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'week7' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'javax.inject:javax.inject:1' + //web 라이브러리 추가 + implementation 'org.springframework.boot:spring-boot-starter-web' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/7 WEEK/seob/gradle/wrapper/gradle-wrapper.jar b/7 WEEK/seob/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/7 WEEK/seob/gradle/wrapper/gradle-wrapper.jar differ diff --git a/7 WEEK/seob/gradle/wrapper/gradle-wrapper.properties b/7 WEEK/seob/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1af9e093 --- /dev/null +++ b/7 WEEK/seob/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/7 WEEK/seob/gradlew b/7 WEEK/seob/gradlew new file mode 100644 index 00000000..1aa94a42 --- /dev/null +++ b/7 WEEK/seob/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/7 WEEK/seob/gradlew.bat b/7 WEEK/seob/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/7 WEEK/seob/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/7 WEEK/seob/settings.gradle b/7 WEEK/seob/settings.gradle new file mode 100644 index 00000000..c7efaebf --- /dev/null +++ b/7 WEEK/seob/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'seob' diff --git a/7 WEEK/seob/src/main/java/week7/seob/SeobApplication.java b/7 WEEK/seob/src/main/java/week7/seob/SeobApplication.java new file mode 100644 index 00000000..f5359e45 --- /dev/null +++ b/7 WEEK/seob/src/main/java/week7/seob/SeobApplication.java @@ -0,0 +1,13 @@ +package week7.seob; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SeobApplication { + + public static void main(String[] args) { + SpringApplication.run(SeobApplication.class, args); + } + +} diff --git a/7 WEEK/seob/src/main/java/week7/seob/common/MyLogger.java b/7 WEEK/seob/src/main/java/week7/seob/common/MyLogger.java new file mode 100644 index 00000000..344f4442 --- /dev/null +++ b/7 WEEK/seob/src/main/java/week7/seob/common/MyLogger.java @@ -0,0 +1,37 @@ +package week7.seob.common; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) +public class MyLogger { + + private String uuid; + private String requestURL; + + public void setRequestURL(String requestURL) { + this.requestURL = requestURL; + } + + public void log(String message) { + System.out.println("[" + uuid + "][" + requestURL + "]" + message); + } + + @PostConstruct + public void init() { + uuid = UUID.randomUUID().toString(); + System.out.println("[" + uuid + "] request scope bean create : " + this); + } + + @PreDestroy + public void close() { + System.out.println("[" + uuid + "] request scope bean close : " + this); + System.out.println(); + } +} diff --git a/7 WEEK/seob/src/main/java/week7/seob/web/LogDemoController.java b/7 WEEK/seob/src/main/java/week7/seob/web/LogDemoController.java new file mode 100644 index 00000000..d834dd0a --- /dev/null +++ b/7 WEEK/seob/src/main/java/week7/seob/web/LogDemoController.java @@ -0,0 +1,34 @@ +package week7.seob.web; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import week7.seob.common.MyLogger; + +@Controller +public class LogDemoController { + private final LogDemoService logDemoService; + private final MyLogger myLogger; + + @Autowired + public LogDemoController(LogDemoService logDemoService, MyLogger myLogger) { + this.logDemoService = logDemoService; + this.myLogger = myLogger; + } + + @RequestMapping("log-demo") + @ResponseBody + public String logDemo(HttpServletRequest request) { + String requestURL = request.getRequestURL().toString(); + System.out.println("myLogger = " + myLogger.getClass()); + myLogger.setRequestURL(requestURL); + + myLogger.log("controller test"); + logDemoService.logic("testId"); + return "OK"; + } + +} diff --git a/7 WEEK/seob/src/main/java/week7/seob/web/LogDemoService.java b/7 WEEK/seob/src/main/java/week7/seob/web/LogDemoService.java new file mode 100644 index 00000000..3ab42255 --- /dev/null +++ b/7 WEEK/seob/src/main/java/week7/seob/web/LogDemoService.java @@ -0,0 +1,21 @@ +package week7.seob.web; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import week7.seob.common.MyLogger; + +@Service +public class LogDemoService { + + private final MyLogger myLogger; + + @Autowired + public LogDemoService(MyLogger myLogger) { + this.myLogger = myLogger; + } + + public void logic(String id) { + myLogger.log("service id = " + id); + } +} diff --git a/7 WEEK/seob/src/main/resources/application.properties b/7 WEEK/seob/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/7 WEEK/seob/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/7 WEEK/seob/src/test/java/week7/seob/SeobApplicationTests.java b/7 WEEK/seob/src/test/java/week7/seob/SeobApplicationTests.java new file mode 100644 index 00000000..bd73d7d1 --- /dev/null +++ b/7 WEEK/seob/src/test/java/week7/seob/SeobApplicationTests.java @@ -0,0 +1,13 @@ +package week7.seob; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SeobApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/7 WEEK/seob/src/test/java/week7/seob/lifecycle/BeanLifeCycleTest.java b/7 WEEK/seob/src/test/java/week7/seob/lifecycle/BeanLifeCycleTest.java new file mode 100644 index 00000000..a9c5cdb2 --- /dev/null +++ b/7 WEEK/seob/src/test/java/week7/seob/lifecycle/BeanLifeCycleTest.java @@ -0,0 +1,29 @@ +package week7.seob.lifecycle; + +import org.junit.jupiter.api.Test; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +public class BeanLifeCycleTest { + + + @Test + public void lifeCycleTest() { + ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class); + NetworkClient client = ac.getBean(NetworkClient.class); + ac.close(); + } + + + @Configuration + static class LifeCycleConfig { + @Bean //(initMethod = "init", destroyMethod = "close") + public NetworkClient networkClient() { + NetworkClient networkClient = new NetworkClient(); + networkClient.setUrl("http://hello-spring.dev"); + return networkClient; + } + } +} diff --git a/7 WEEK/seob/src/test/java/week7/seob/lifecycle/NetworkClient.java b/7 WEEK/seob/src/test/java/week7/seob/lifecycle/NetworkClient.java new file mode 100644 index 00000000..c3033f6a --- /dev/null +++ b/7 WEEK/seob/src/test/java/week7/seob/lifecycle/NetworkClient.java @@ -0,0 +1,47 @@ +package week7.seob.lifecycle; + + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +public class NetworkClient { + + private String url; + + public NetworkClient() { + System.out.println("생성자 호출, url = " + url); + } + + public void setUrl(String url) { + this.url = url; + } + + //서비스 시작시 호출 + public void connect() { + System.out.println("connect: " + url); + } + + public void call(String message) { + System.out.println("call: " + url + " message = " + message); + } + + //서비스 종료시 호출 + public void disconnect() { + System.out.println("close: " + url); + } + + //의존관계 주입이 끝난 후 + @PostConstruct + public void init() { + System.out.println("\nNetworkClient.init"); + connect(); + call("초기화 연결 메시지"); + } + + //Bean이 종료될 때 + @PreDestroy + public void close() { + System.out.println("\nNetworkClient.close"); + disconnect(); + } +} diff --git a/7 WEEK/seob/src/test/java/week7/seob/scope/PrototypeTest.java b/7 WEEK/seob/src/test/java/week7/seob/scope/PrototypeTest.java new file mode 100644 index 00000000..b78e39bb --- /dev/null +++ b/7 WEEK/seob/src/test/java/week7/seob/scope/PrototypeTest.java @@ -0,0 +1,39 @@ +package week7.seob.scope; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Scope; + +public class PrototypeTest { + + @Test + void prototypeBeanFind() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class); + System.out.println("find prototypeBean1"); + PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); + System.out.println("find prototypeBean2"); + PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); + + System.out.println("prototypeBean1 = " + prototypeBean1); + System.out.println("prototypeBean2 = " + prototypeBean2); + Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2); + + ac.close(); + } + + @Scope("prototype") + static class PrototypeBean { + @PostConstruct + public void init() { + System.out.println("PrototypeBean.init"); + } + + @PreDestroy + public void destroy() { + System.out.println("PrototypeBean.destroy"); + } + } +} diff --git a/7 WEEK/seob/src/test/java/week7/seob/scope/SingletonTest.java b/7 WEEK/seob/src/test/java/week7/seob/scope/SingletonTest.java new file mode 100644 index 00000000..905cb248 --- /dev/null +++ b/7 WEEK/seob/src/test/java/week7/seob/scope/SingletonTest.java @@ -0,0 +1,38 @@ +package week7.seob.scope; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Scope; + +public class SingletonTest { + + @Test + void singletonBeanFind() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class); + SingletonBean singletonBean1 = ac.getBean(SingletonBean.class); + SingletonBean singletonBean2 = ac.getBean(SingletonBean.class); + + System.out.println("singletonBean1 = " + singletonBean1); + System.out.println("singletonBean2 = " + singletonBean2); + Assertions.assertThat(singletonBean1).isEqualTo(singletonBean2); + + ac.close(); + } + + @Scope("singleton") + static class SingletonBean{ + @PostConstruct + public void init() { + System.out.println("SingletonBean.init"); + } + + @PreDestroy + public void destroy() { + System.out.println("SingletonBean.destroy"); + } + } + +} diff --git a/7 WEEK/seob/src/test/java/week7/seob/scope/SingletonWithPrototypeTest1.java b/7 WEEK/seob/src/test/java/week7/seob/scope/SingletonWithPrototypeTest1.java new file mode 100644 index 00000000..25a1b193 --- /dev/null +++ b/7 WEEK/seob/src/test/java/week7/seob/scope/SingletonWithPrototypeTest1.java @@ -0,0 +1,78 @@ +package week7.seob.scope; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Scope; + +import javax.inject.Provider; + +public class SingletonWithPrototypeTest1 { + + @Test + void prototypeFind() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class); + + PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); + prototypeBean1.addCount(); + Assertions.assertThat(prototypeBean1.getCount()).isEqualTo(1); + + PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); + prototypeBean2.addCount(); + Assertions.assertThat(prototypeBean2.getCount()).isEqualTo(1); + } + + @Test + void singletonClientUsePrototype() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class); + + ClientBean clientBean1 = ac.getBean(ClientBean.class); + int count1 = clientBean1.logic(); + Assertions.assertThat(count1).isEqualTo(1); + + ClientBean clientBean2 = ac.getBean(ClientBean.class); + int count2 = clientBean2.logic(); + Assertions.assertThat(count2).isEqualTo(1); + + } + + @Scope("singleton") + static class ClientBean{ + + @Autowired + private Provider prototypeBeanProvider; + + public int logic() { + PrototypeBean prototypeBean = prototypeBeanProvider.get(); + prototypeBean.addCount(); + int count = prototypeBean.getCount(); + return count; + } + } + @Scope("prototype") + static class PrototypeBean { + private int count = 0; + + public void addCount() { + count++; + } + + public int getCount() { + return count; + } + + @PostConstruct + public void init() { + System.out.println("PrototypeBean.init " + this); + } + + @PreDestroy + public void destroy() {//호출 X + System.out.println("PrototypeBean.destory "+ this); + } + } +} diff --git a/8 WEEK/servlet/.gitignore b/8 WEEK/servlet/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/8 WEEK/servlet/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/8 WEEK/servlet/SECTION1.md b/8 WEEK/servlet/SECTION1.md new file mode 100644 index 00000000..4f8c2930 --- /dev/null +++ b/8 WEEK/servlet/SECTION1.md @@ -0,0 +1,201 @@ +# 스프링 웹 MVC + +## 웹 서버, 웹 애플리케이션 서버 + +### 모든 것이 HTTP +HTTP 메시지에 모든 것을 전송 +- HTML, TEXT +- IMAGE, 음성, 영상, 파일 +- JSON, XML(API) +- 거의 모든 형태의 데이터 전송 가능 +- 서버간에 데이터를 주고 받을 때도 대부분 HTTP 사용 +- **지금은 HTTP 시대** + +### 웹 서버(Web Server) +- HTTP 기반으로 동작 +- 정적 리소스 제공, 기타 부가기능 +- 정적(파일) HTML, CSS, JS, 이미지, 영상 +- 예) NGINX, APACHE + +### 웹 애플리케이션 서버(WAS - Web Application Server) +- HTTP 기반으로 동작 +- 웹 서버 기능 포함 + (정적 리소스 제공 가능) +- 프로그램 코드를 실행해서 애플리케이션 로직 수행 + - 동적 HTML, HTTP API(JSON) + - Servlet, Jsp, Spring MVC +- 예) Tomcat, Jetty, Undertow + +### 웹 서버, 웹 애플리케이션 서버(WAS) 차이 +- 웹 서버는 정적 리소스(파일), WAS는 애플리케이션 로직 +- 둘의 용어 경계가 모호함 + - WS 서버도 프로그램을 실행하는 기능을 포함하기도 함 + - WAS도 WS의 기능을 제공 +- 자바는 서블릿 컨테이너 기능을 제공하면 WAS + - 서블릿 없이 자바코드를 실행하는 서버 프레임워크도 존재 + + +### 웹 시스템 구성 - WAS, DB +- WAS, DB만으로 시스템 구성 가능 +- WAS는 정적 리소스, 애플리케이션 로직 모두 제공 가능 + + +- WAS가 너무 많은 역할을 담당 -> 서버 과부하 우려 +- 가장 비싼 애플리케이션 로직이 정적 리소스 때문에 수행이 어려울 수 있음 +- WAS 장애시 오류 화면도 노출 불가 + +### 웹 시스템 구성 - WEB, WAS, DB +- 정적 리소스는 웹 서버가 처리 +- 웹 서버는 애플리케이션 로직같은 동적인 처리가 필요하면 WAS에 요청을 위임 +- WAS는 중요한 애플리케이션 로직 처리 전담 + +>WS = HTML, CSS, JS, 이미지 처리
+>WAS = 애플리케이션 로직 처리 + +- 효율적인 리소스 관리 + - 정적 리소스가 많이 사용되면 WS 증설 + - 애플리케이션 리소스가 많이 사용되면 WAS 증설 +- 정적 리소스만 제공하는 WS는 잘 죽지 않음 +- 애플리캐이션 로직이 동작하는 WAS서버는 잘 죽음 +- WAS, DB 장애시 WEB 서버가 오류 화면 제공 가능 + + +## Servlet +### HTML Form 데이터 전송 +#### POST 전송 - 저장 +![S1-1.png](img%2FS1-1.png) + + +### 서버에서 처리해야 하는 업무 +#### 웹 애플리케이션 서버 직접 구현시 + - ![S1-2.png](img%2FS1-2.png) +#### 서블릿을 지원하는 WAS 사용시 + - ![S1-3.png](img%2FS1-3.png) + +### 서블릿 +#### 특징 +```java +@WebServlet(name = "helloServlet", urlPatterns = "/hello") +public class HelloServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response){ + //애플리케이션 로직 + } +``` +- urlPatterns(/hello)의 URL 호출되면 서블릿 코드 실행 +- HTTP 요청 정보를 편리하게 사용할 수 있는 HttpServletRequest +- HTTP 응답 정보를 편리하게 제공할 수 있는 HttpServletResponse +![S1-4.png](img%2FS1-4.png) + +#### HTTP 요청, 응답 흐름 +- HTTP 요청시 + - WAS는 Request, Response 객체를 새로 만들어 서블릿 객체 호출 + - 개발자는 Request객체에서 HTTP요청 정보를 편리하게 꺼내 사용 + - 개발자는 Response 객체에 HTTP 응답 정보를 편리하게 입력 + - WAS는 Response 객체에 담겨있는 내용으로 HTTP 응답 정보를 생성 + + +#### 서블릿 컨테이너 +![S1-5.png](img%2FS1-5.png) + +- 톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 함 +- 서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기 관리 +- 서블릿 객체는 싱글톤으로 관리 + - 고객의 요청이 올 때 마다 계속 객체를 생성하는 것은 비효율 + - 최초 로딩 시점에 서블릿 객체를 미리 만들어두고 재활용 + - 모든 고객 요청은 동일한 서블릿 객체 인스턴스에 접근 + - 공유 변수 사용 주의 + - 서블릿 컨테이너 종료시 함께 종료 +- JSP도 서블릿으로 변환 되어서 사용 +- 동시 요청을 위한 멀티 스레드 처리 지원 + + +## 동시 요청 - 멀티 스레드 + +### 스레드 +- 애플리케이션 코드를 하나하나 순차적으로 실행하는 것 = 스레드 +- 자바 메인 메서드 첫 실행 시 main이라는 이름의 스레드 실행 +- 스레드가 없다면 자바 애플리케이션 실행 불가 +- 스레드는 한번에 하나의 코드 라인 실행 +- 동시 처리가 필요하면 스레드를 추가로 생성 + +### 요청 마다 스레드 생성 + +- 장점 + - 동시 요청 처리 가능 + - 리소스(CPU, 메모리)가 허용할 때 까지 처리 가능 + - 하나의 스레드 지연 돼도, 나머지 스레드 정상 작동 +- 단점 + - 스레드는 생성 비용이 매우 비쌈 + - 고객의 요청이 올 때마다 스레드 생성 시, 응답 속도가 느림 + - 스레드는 컨텍스트 스위칭 비용 발생 + - 스레드 생성에 제한이 없음. + - 고객 요청이 너무 많이 오면, CPU, 메모리 임계점을 넘어 서버가 죽을 수 있다. + +### 스레드 풀 +#### 요청 마다 스레드 생성의 단점 보완 +- 특징 + - 필요한 스레드를 스레드 풀에 보관하고 관리. + - 스레드 풀에 생성 가능한 스레드의 최대치를 관리. 톰캣은 최대 200개 기본 설정 +- 사용 + - 스레드가 필요하면, 이미 생성되어 있는 스레드를 스레드 풀에서 꺼내 사용. + - 사용을 종료하면 스레드 풀에 해당 스레드 반납. + - 최대 스레드가 모두 사용중이어서 스레드 풀에 스레드가 없다면 + - 기다리는 요청은 거절하거나 특정 숫자만틈만 대기하도록 설정 가능 +- 장점 + - 스레드가 미리 생성돼 있어, 스레드를 생성하고 종료하는 비용(CPU)가 절약, 응답 시간이 빠름 + - 생성 가능한 스레드의 최대치가 있어 너무 많은 요청이 들어와도 기존 요청을 안전하게 처리 가능 + +### WAS의 멀티 스레드 지원 +- 멀티 스레드에 대한 부분은 WAS가 처리 +- 개발자가 멀티 스레드 관련 코드를 신경쓰지 않아도 됨 +- 개발자는 마치 싱글 스레드 프로그래밍 하듯 편리하게 코드 개발 가능 +- 멀티 스레드 환경이므로 싱글톤 객체(서블릿, 스프링 빈)는 주의해서 사용 + + + +## HTML, HTTP API, CSR, SSR + +### 정적 리소스 +- 고정된 HTML 파일, CSS, JS, 이미지, 영상 등을 제공 +- 주로 웹 브라우저 + +### HTML 페이지 +- 동적으로 필요한 HTML 파일을 생성해 전달 +- 웹 브라우저 : HTML 해석 + +### HTML API +- HTML이 아니라 데이터 전달 +- 주로 JSON 형식 사용 +- 다양한 시스템에서 호출 +- 데이터만 주고 받음, UI 화면이 필요하면, 클라이언트가 별도 처리 +- 앱, 웹 클라이언트, 서버 to 서버 +![S1-6.png](img%2FS1-6.png) + +#### 다양한 시스템 연동 +- 주로 JSON 형태로 데이터 통신 +- UI클라이언트 접점 + - 앱 클라이언트(아이폰, 안드로이드, PC앱) + - 웹 브라우저에서 JS를 통한 HTTP API 호출 + - React, Vue.js 같은 웹 클라이언트 +- 서버 to 서버 + - 주문 서버 -> 결제 서버 + - 기업간 데이터 통신 + +### 서버 사이드 렌더링, 클라이언트 사이드 렌더링 +#### - SSR - 서버 사이드 렌더링 + - HTML 최종 결과를 서버에서 만들어 웹 브라우저에 전달 + - 주로 정적인 화면에 사용 + - 관련기술 : JSP, 타임리프 -> BE 개발자 +![S1-7.png](img%2FS1-7.png) + +#### - CSR - 클라이언트 사이드 렌더링 + - HTML 결과를 자바스크립트를 사용해 웹 브라우저에서 동적으로 생성해 적용 + - 주로 동적인 화면에 사용, 웹 환경을 마치 앱 처럼 필요한 부분만 변경할 수 있음 + - 예) 구글지도, Gmail, 구글 캘린더 + - 관련기술 : React, Vue.js -> FE 개발자 +![S1-8.png](img%2FS1-8.png) + + +- 참고 + - React, Vue.js를 CSR + SSR 동시에 진원하는 웹 프레임워크 존재 + - SSR을 사용하더라도 자바스크립트를 사용해 화면 일부를 동적으로 변경 가능 diff --git a/8 WEEK/servlet/SECTION2.md b/8 WEEK/servlet/SECTION2.md new file mode 100644 index 00000000..17c3e7e9 --- /dev/null +++ b/8 WEEK/servlet/SECTION2.md @@ -0,0 +1,826 @@ +# 서블릿 + + +## Hello 서블릿 + +> **참고**
+> 서블릿은 톰캣 같은 WAS 서버를 직접 설치, 그 위에 서블리 ㅅ코드를 클래스 파일로 빌드해서 +> 올린 다음, 톰캣 서버를 실행한다. 이 과정은 매우 번거롭다.
+> 스프링 부트는 톰캣 서버를 내장하고 있으므로, 톰캣 서버 설치 없이 편리하게 서블릿 코드를 실행할 수 있다. + + +### 스프링 부트 서블릿 환경 구성 + +`@ServletComponentScan`
+스프링 부트는 서블릿을 직접 등록해서 사용할 수 있도록 `@ServletComponentScan`을 지원한다. +다음과 같이 추가한다. + +*ServletApplication* +```java +package hello.servlet; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.ServletComponentScan; + +@ServletComponentScan //서블릿 자동 등록 +@SpringBootApplication +public class ServletApplication { + + public static void main(String[] args) { + SpringApplication.run(ServletApplication.class, args); + } + +} +``` +
+ +**서블릿 등록하기** + +*HelloServlet* +```java +package hello.servlet.basic; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "helloServlet", urlPatterns = "/hello") +public class HelloServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException { + System.out.println("HelloServlet.service"); + System.out.println("requset = " + requset); + System.out.println("response = " + response); + + String username = requset.getParameter("username"); + System.out.println("username = " + username); + + response.setContentType("text/plain"); + response.setCharacterEncoding("utf-8"); + response.getWriter().write("hello " + username); + } +} +``` + +- `@WebServlet` 서블릿 애노테이션 + - name : 서블릿 이름 + - urlPatterns : URL매핑 + +HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 다음 메서드를 실행
+`protected void service(HttpServletRequest requset, HttpServletResponse response)` + +- 웹 브라우저 실행 + - http://localhost8080/hello?username=world + - 결과 : hello world + +- 콘솔 실행결과 +``` +HelloServlet.service +requset = org.apache.catalina.connector.RequestFacade@42a1a9b6 +response = org.apache.catalina.connector.ResponseFacade@e7a3bb +username = world +``` + + +### 서블릿 컨테이너 동작 방식 +**내장 톰캣 서버 생성** +![S2-1.png](img%2FS2-1.png) + +**HTTP 요청, HTTP응답 메시지** +![S2-2.png](img%2FS2-2.png) + +**웹 애플리케이션 서버의 요청 응답 구조** +![S2-3.png](img%2FS2-3.png) + +> **참고**
+> HTTP응답에서 Content-Length는 웹 애플리케이션 서버가 자동으로 생성해준다. + + +### welcome 페이지 추가 + +```html + + + + + Title + + + + + +``` + + +## HttpServletRequest - 개요 + +**HttpServletRequest 역할**
+HTTP 요청 메시지를 개발자가 직접 파싱해도 사용해도 되지만, 매우 불편하다. 서블릿은 개발자가 HTTP 요청 메시지를 +편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱한다. 그리고 그 결과를 `HttpServletRequest`객체에 담아서 제공한다. + +HttpServletRequest를 사용하면 다음과 같은 HTTP 요청 메시지를 편리하게 조회할 수 있다. + +*HTTP 요청 메시지* +``` +POST /save HTTP/1.1 +Host: localhost:8080 +Content-Type: application/x-www-form-urlencoded +username=kim&age=20 +``` + +- START LINE + - HTTP 메서드 + - URL + - 쿼리 스트링 + - 스키마, 프로토콜 +- 헤더 + - 헤더 조회 +- 바디 + - form 파라미터 형식 조회 + - message body 데이터 직접 조회 + +HttpServletRequest 객체는 추가로 여러가지 부가기능도 함께 제공한다. + +**임시 저장소 기능** +- 해당 HTTP 요청이 시작부터 끝날 때 까지 유지되는 임시 저장소 기능 + - 저장 : `request.setAttribute(name, value)` + - 조회 : `request.getAttribute(name)` + +**세션 관리 기능** +- `request.getSession(create: true)` + +> **중요**
+> HttpServletRequest, HttpServletResponse를 사용할 때 가장 중요한 점은 이 객체들이 HTTP 요청 메시지, +> HTTP 응답 메시지를 편리하게 사용하도록 도와주는 객체라는 점이다. 따라서 이 기능에 대해 깊이 이해하려면 +> **HTTP 스팩이 제공하는 요청, 응답 메시지 자체를 이해**해야 한다. + + + +## HttpServletRequest - 기본 사용법 +HttpServletRequest가 제공하는 기본 기능 + +*RequestHeaderServlet* +```java +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header") +public class RequestHeaderServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + printStartLine(request); + printHeaders(request); + printHeaderUtils(request); + printEtc(request); + + } + + //start line 정보 + private void printStartLine(HttpServletRequest request) { + System.out.println("--- REQUEST-LINE - start ---"); + System.out.println("request.getMethod() = " + request.getMethod()); //GET + System.out.println("request.getProtocol() = " + request.getProtocol()); //HTTP/1.1 + System.out.println("request.getScheme() = " + request.getScheme()); //http + // http://localhost:8080/request-header + System.out.println("request.getRequestURL() = " + request.getRequestURL()); + // /request-header + System.out.println("request.getRequestURI() = " + request.getRequestURI()); + //username=hi + System.out.println("request.getQueryString() = " + + request.getQueryString()); + System.out.println("request.isSecure() = " + request.isSecure()); //https 사용유무 + System.out.println("--- REQUEST-LINE - end ---"); + System.out.println(); + } + + //Header 모든 정보 + private void printHeaders(HttpServletRequest request) { + System.out.println("--- Headers - start ---"); + /* + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + System.out.println(headerName + ": " + request.getHeader(headerName)); + } + */ + request.getHeaderNames().asIterator() + .forEachRemaining(headerName -> System.out.println(headerName + ": " + + request.getHeader(headerName))); + System.out.println("--- Headers - end ---"); + System.out.println(); + } + + + //Header 편리한 조회 + private void printHeaderUtils(HttpServletRequest request) { + System.out.println("--- Header 편의 조회 start ---"); + System.out.println("[Host 편의 조회]"); + System.out.println("request.getServerName() = " + + request.getServerName()); //Host 헤더 + System.out.println("request.getServerPort() = " + + request.getServerPort()); //Host 헤더 + System.out.println(); + System.out.println("[Accept-Language 편의 조회]"); + request.getLocales().asIterator() + .forEachRemaining(locale -> System.out.println("locale = " + + locale)); + System.out.println("request.getLocale() = " + request.getLocale()); + System.out.println(); + System.out.println("[cookie 편의 조회]"); + if (request.getCookies() != null) { + for (Cookie cookie : request.getCookies()) { + System.out.println(cookie.getName() + ": " + cookie.getValue()); + } + } + System.out.println(); + System.out.println("[Content 편의 조회]"); + System.out.println("request.getContentType() = " + + request.getContentType()); + System.out.println("request.getContentLength() = " + + request.getContentLength()); + System.out.println("request.getCharacterEncoding() = " + + request.getCharacterEncoding()); + } + + //기타 정보 + private void printEtc(HttpServletRequest request) { + System.out.println("--- 기타 조회 start ---"); + System.out.println("[Remote 정보]"); + System.out.println("request.getRemoteHost() = " + + request.getRemoteHost()); // + System.out.println("request.getRemoteAddr() = " + + request.getRemoteAddr()); // + System.out.println("request.getRemotePort() = " + + request.getRemotePort()); // + System.out.println(); + System.out.println("[Local 정보]"); + System.out.println("request.getLocalName() = " + request.getLocalName()); // + System.out.println("request.getLocalAddr() = " + request.getLocalAddr()); // + System.out.println("request.getLocalPort() = " + request.getLocalPort()); // + System.out.println("--- 기타 조회 end ---"); + System.out.println(); + } +} +``` +실행결과 +``` +--- REQUEST-LINE - start --- +request.getMethod() = GET +request.getProtocol() = HTTP/1.1 +request.getScheme() = http +request.getRequestURL() = http://localhost:8080/request-header +request.getRequestURI() = /request-header +request.getQueryString() = null +request.isSecure() = false +--- REQUEST-LINE - end --- + +--- Headers - start --- +host: localhost:8080 +connection: keep-alive +cache-control: max-age=0 +sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120" +sec-ch-ua-mobile: ?0 +sec-ch-ua-platform: "Windows" +upgrade-insecure-requests: 1 +user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 +accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 +sec-fetch-site: same-origin +sec-fetch-mode: navigate +sec-fetch-user: ?1 +sec-fetch-dest: document +referer: http://localhost:8080/basic.html +accept-encoding: gzip, deflate, br +accept-language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7 +cookie: Idea-1afd2dea=715e6892-4a57-4307-afc0-f323498e55fb +--- Headers - end --- + +--- Header 편의 조회 start --- +[Host 편의 조회] +request.getServerName() = localhost +request.getServerPort() = 8080 + +[Accept-Language 편의 조회] +locale = ko_KR +locale = ko +locale = en_US +locale = en +request.getLocale() = ko_KR + +[cookie 편의 조회] +Idea-1afd2dea: 715e6892-4a57-4307-afc0-f323498e55fb + +[Content 편의 조회] +request.getContentType() = null +request.getContentLength() = -1 +request.getCharacterEncoding() = UTF-8 +--- 기타 조회 start --- +[Remote 정보] +request.getRemoteHost() = 0:0:0:0:0:0:0:1 +request.getRemoteAddr() = 0:0:0:0:0:0:0:1 +request.getRemotePort() = 57354 + +[Local 정보] +request.getLocalName() = 0:0:0:0:0:0:0:1 +request.getLocalAddr() = 0:0:0:0:0:0:0:1 +request.getLocalPort() = 8080 +--- 기타 조회 end --- +``` + +> **참고**
+> 로컬에서 테스트하면 IPv6정보가 나오는데, IPv4 정보를 보고싶다면 다음 옵션을 VM options에 넣어주면 된다. +>
`-Djava.net.preferIPv4Stack=true` + + +## HTTP 요청 데이터 - 개요 +HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법이 있다. + +**주로 다음 3가지 방법을 사용** +- **GET - 쿼리 파라미터** + - /url?username=seob&age=24 + - 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달 + - 예)검색, 필터, 페이징등등에서 많이 사용하는 방식 +- **POST - HTML Form** + - content-type : application/x-www-form-urlencoded + - 메시지 바디에 쿼리 파라미터 형식으로 전달 username=seob&age=24 + - 예)회원 가입, 상품 주문, HTML Form 사용 +- **HTTP message body**에 데이터를 직접 담아서 요청 + - HTTP API에서 주로 사용, JSON, XML, TEXT + - 데이터 형식은 주로 JSON에 사용 + - POST, PUT, PATCH + + +### HTTP 요청 데이터 - GET 쿼리 파라미터 + +전달 데이터 +- username=hello +- age=20 + +메시지 바디 없이, URL의 **쿼리 파라미터**를 사용해서 데이터 전달 +- 예)검색, 필터, 페이징등등에서 많이 사용하는 방식 + +쿼리 파라미터는 URL에 다음과 같이 `?`를 시작으로 보낼 수 있다. 추가 파라미터는 `&`로 구분 +- http://localhost:8080/request-param?username=hello&age=20 + +*RequestParamServlet* +```java +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Enumeration; + +@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param") +public class RequestParamServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException { + System.out.println("[전체 파라미터 조회] - start"); + +// Enumeration parameterNames = requset.getParameterNames(); + requset.getParameterNames().asIterator().forEachRemaining( + paramName -> System.out.println(paramName + "=" + requset.getParameter(paramName)) + ); + + System.out.println("[전체 파라미터 조회] - end"); + System.out.println(); + + System.out.println("[단일 파라미터 조회]"); + String username= requset.getParameter("username"); + String age = requset.getParameter("age"); + + System.out.println("username = " + username); + System.out.println("age = " + age); + + System.out.println("[이름이 같은 복수 파라미터 조회]"); + String[] usernames = requset.getParameterValues("username"); + for(String name: usernames) { + System.out.println("username = " + name); + } + } +} +``` +실행결과 - 파라미터 전송
+http://localhost:8080/request-param?username=hello&age=20 +``` +[전체 파라미터 조회] - start +username=hello +age=20 +[전체 파라미터 조회] - end + +[단일 파라미터 조회] +username = hello +age = 20 +[이름이 같은 복수 파라미터 조회] +username = hello +``` +
+ +실행결과 - 동일 파라미터 전송 +``` +[전체 파라미터 조회] - start +username=hello +age=20 +[전체 파라미터 조회] - end +[단일 파라미터 조회] +request.getParameter(username) = hello +request.getParameter(age) = 20 +[이름이 같은 복수 파라미터 조회] +request.getParameterValues(username) +username=hello +username=Lee +``` + +파라미터의 값은 하나인데, 값이 중복이면 `request.getParameterValue()` 를 사용해야한다.
+중복일 때 `request.getParameter()`를 사용하면 `request.getParameterValue()`의 첫 번째 값을 반환한다. + + +## HTTP 요청 데이터 - POST HTML FORM +주로 회원가입, 상품주문 등에서 사용 + +**특징** +- content-type : `application/x-www-form-urlencode` +- 메시지 바디에 쿼리 파라미터 형식으로 데이터를 전달. `username=hello&age=20` + +*hello-form.html* +```html + + + + + Title + + +
+ username: + age: + +
+ + +``` + +실행결과 + +![S2-4.png](img%2FS2-4.png) + +전송버튼 클릭 + +![S2-5.png](img%2FS2-5.png) + +POST의 HTML FORM을 전송하면 웹 브라우저는 다음 형식으로 HTTP 메시지를 만든다. +- **요청 URL** : http://localhost:8080/request-param +- **content-type** : `application/x-www-form-urlencoded` +- **message body** : `username=hello&age=20` + +`application/x-www-form-urlencoded`형식은 앞서 GET에서 살펴본 쿼리 파라미터 형식과 같다. +따라서 **쿼리 파라미터 조회 메서드를 그대로 사용**하면 된다. + +`request.getParameter()`는 GET URL 쿼리 파라미터 형식도 지원, POST HTML FORM 형식도 지원한다. + +> **참고**
+> content-type은 HTTP메시지 바디에 데이터 형식을 지정.
+> **GET URL 쿼리 파라미터 형식**으로 클라이언트에서 서버로 데이터를 전달할 때는 HTTP 메시지 바디를 사용하지 않기 때문에 content-type이 없다.
+> **POST FHTML Form 형식**으로 데이터를 전달하면 HTTP 메시지 바디에 해당 데이터를 포함해서 보내기 때문에 바디에 포함된 +> 데이터가 어떤 형식인지 content-type을 반드시 지정해야 한다. 이렇게 폼으로 데이터를 전송하는 형식을 `application/x-www-form-urlencoded`라 한다. + + +### Postman을 사용한 테스트 +Postman을 사용하면 굳이 HTML form 을 생성하지 않고 테스트 가능하다. +![S2-6.png](img%2FS2-6.png) + + +## HTTP 요청 데이터 - API 메시지 바디 - 단순 텍스트 +- **HTTP message body**에 데이터를 직접 담아서 요청 + - HTTP API에서 주로 사용, JSON, XML, TEXT + - 데이터 형식은 주로 JSON사용 + - POST, PUT, PATCH + + +- 먼저 가장 단순한 텍스트 메시지를 HTTP 메시지에 담아 전송하고 읽기. +- HTTP 메시지 바디의 데이터를 InputStream을 사용해 직접 읽을 수 있다. + +*RequestBodyStringServlet* +```java +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@WebServlet(name="RequestBodyStringServlet", urlPatterns = "/request-body-string") +public class RequestBodyStringServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + + System.out.println("messageBody = " + messageBody); + + response.getWriter().write("OK"); + } +} +``` + +Postman 테스트 + +![S2-7.png](img%2FS2-7.png) + +문자 전송 +- POST http://localhost:8080/request-body-string +- content-type: text/plain +- message body: hello + +실행결과 +``` +messageBody = helloooo! +``` + +> **참고**
+> inputStream은 byte코드로 반환. byte코드를 우리가 읽을 수 있는 문자(String)로 보려면 +> 문자표(Charset)를 지정해야함. 여기서 UTF_8을 지정 + + + +## HTTP 요청 데이터 - API 메시지 바디 - JSON +HTTP API에서 주로 사용하는 JSON형식으로 데이터 전달해보기 + +**JSON 형식 전송** +- content-type : **application/json** +- message body : `{"username": "hello","age": 20}` +- 결과 : `messageBody = {"username":"hello","age":20}` + +**JSON 형식 바싱 추가** + +*HelloData* +```java +package hello.servlet.basic; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class HelloData { + private String username; + private int age; +} +``` + +*RequestBodyJsonServlet* +```java +package hello.servlet.basic.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import hello.servlet.basic.HelloData; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json") +public class RequestBodyJsonServlet extends HttpServlet { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + + System.out.println("messageBody = " + messageBody); + + HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); + + System.out.println("helloData.username = " + helloData.getUsername()); + System.out.println("helloData.age = " + helloData.getAge()); + + response.getWriter().write("ok"); + } +} + +``` +Postman으로 실행 + +![S2-8.png](img%2FS2-8.png) + +실행결과 +``` +messageBody = {"username": "hello","age": 20} +helloData.username = hello +helloData.age = 20 +``` + +> **참고**
+> JSON결과를 파싱해서 사용할 수 있는 자바 객체로 변환하려면 Jackson, Gson 같은 JSON 변환 라이브러리를 추가해서 사용해야한다. +> Spring MVC를 선택하면 기본으로 Jackson 라이브러리(ObjectMapper)를 함께 제공한다. + + +> **참고**
+> HTML form 데이터도 메시지 바디를 통해 전송되므로 직접 읽을 수 있다. 하지만 편리한 파라미터 조회기능 +> (request.getParameter(...))을 이미 제공하기 때문에 파라미터 조회 기능을 사용하면 된다. + + + +## HttpServletResponse - 기본 사용법 +### HttpServletResponse 역할 + +**HTTP 응답 메시지 생성** +- HTTP 응답 코드 지정 +- 헤더 생성 +- 바디 생성 + +**편의 기능 제공** +- Content-Type, 쿠키, Redirect + + +### HttpServletResponse - 기본 사용법 + +*ResponseHeaderServlet* +```java +package hello.servlet.basic.response; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header") +public class ResponseHeaderServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + //[status-line] + response.setStatus(HttpServletResponse.SC_OK); + + //[response-headers] + response.setHeader("Content-Type", "text/plain;charset=utf-8"); + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("my-header", "hello"); + + //[Header 편의 메서드] +// content(response); +// cookie(response); +// redirect(response); + + //[message body] + PrintWriter writer = response.getWriter(); + writer.println("ok"); + } +} +``` + +*Content 편의 메서드* +```java + private void content(HttpServletResponse response) { + //Content-Type: text/plain;charset=utf-8 + //Content-Length: 2 + //response.setHeader("Content-Type", "text/plain;charset=utf-8"); + response.setContentType("text/plain"); + response.setCharacterEncoding("utf-8"); + //response.setContentLength(2); //(생략시 자동 생성) + } +``` + +*Cookie 편의 메서드* +```java + private void cookie(HttpServletResponse response) { + //Set-Cookie: myCookie=good; Max-Age=600; + //response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600"); + Cookie cookie = new Cookie("myCookie", "good"); + cookie.setMaxAge(600); //600초 + response.addCookie(cookie); + } +``` + +*Redirect 편의 메서드* +```java + private void redirect(HttpServletResponse response) throws IOException { + //Status Code 302 + //Location: /basic/hello-form.html + //response.setStatus(HttpServletResponse.SC_FOUND); //302 + //response.setHeader("Location", "/basic/hello-form.html"); + response.sendRedirect("/basic/hello-form.html"); + } +``` + + +## HTTP 응답 데이터 - 단순 텍스트, HTML +HTTP 응답 메시지는 주로 아래와 같은 내용을 담아 전달 +- 단순 텍스트 응답 + - `writer.println("ok");` +- HTML 응답 +- HTML API - MessageBody JSON 응답 + +### HttpServletResponse - HTML 응답 + +*ResponseHtmlServlet* +```java +package hello.servlet.basic.response; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html") +public class ResponseHtmlServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + //Content-Type: text/html;charset=utf-8 + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + PrintWriter writer = response.getWriter(); + writer.println(""); + writer.println(""); + writer.println("
HI?
"); + writer.println(""); + writer.println(""); + } +} +``` + +HTTP 응답으로 HTML을 반환할 때 content-type을 `text/html`로 지정해야 한다. + + +## HTTP 응답 데이터 - API JSON + + +*ResponseJsonServlet* +```java +package hello.servlet.basic.response; + +import com.fasterxml.jackson.databind.ObjectMapper; +import hello.servlet.basic.HelloData; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json") +public class ResponseJsonServlet extends HttpServlet { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + + HelloData helloData = new HelloData(); + helloData.setUsername("lee"); + helloData.setAge(20); + + String result = objectMapper.writeValueAsString(helloData); + response.getWriter().write(result); + } +} +``` + +HTTP 응답으로 JSON을 반환할 때 content-type을 `application/json`로 지정해야 한다.
+Jackson 라이브러리가 제공하는 `objectMapper.writeValueAsString()`를 사용하면 객체를 JSON 문자로 변경할 수 있다. + + diff --git a/8 WEEK/servlet/SECTION3.md b/8 WEEK/servlet/SECTION3.md new file mode 100644 index 00000000..5f56ad9a --- /dev/null +++ b/8 WEEK/servlet/SECTION3.md @@ -0,0 +1,803 @@ +# 3. 서블릿, JSP, MVC 패턴 + + +## 회원 관리 웹 애플리케이션 요구사항 + +회원정보 - 이름, 나이
+기능 요구사항 - 회원 저장, 회원 목록 조회 + +*Member* +```java +package hello.servlet.domain.member; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Member { + + private Long id; + private String username; + private int age; + + public Member(String username, int age) { + this.username = username; + this.age = age; + } +} +``` + + +*MemberRepository* +```java +package hello.servlet.domain.member; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MemberRepository { + + private static Map store = new HashMap<>(); + private static long sequence = 0L; + + //싱글톤 + private static final MemberRepository instance = new MemberRepository(); + + public static MemberRepository getInstance() { + return instance; + } + private MemberRepository(){} + + public Member save(Member member) { + member.setId(++sequence); + store.put(member.getId(), member); + return member; + } + + public Member findById(Long id) { + return store.get(id); + } + + public List findAll() { + return new ArrayList<>(store.values()); + } + + public void clearStore() { + store.clear(); + } +} +``` + +싱글톤 패턴 적용. 싱글톤 패턴은 객체를 단 하나만 생성해서 공유해야 하므로 생성자를 private 접근자로 막아둔다. + + +*MemberRepositoryTest* +```java +package hello.servlet.domain.member; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MemberRepositoryTest { + + MemberRepository memberRepository = MemberRepository.getInstance(); + + @AfterEach + void afterEach() { + memberRepository.clearStore(); + } + + @Test + void save() { + Member member = new Member("Hello", 20); + + Member savedMember = memberRepository.save(member); + + Member findMember = memberRepository.findById(savedMember.getId()); + Assertions.assertThat(findMember).isEqualTo(savedMember); + + } + + @Test + void findAll() { + Member member1 = new Member("Hello1", 20); + Member member2 = new Member("Hello2", 20); + + memberRepository.save(member1); + memberRepository.save(member2); + + List memberList = memberRepository.findAll(); + + Assertions.assertThat(memberList.size()).isEqualTo(2); + Assertions.assertThat(memberList).contains(member1, member2); + } + + +} +``` + + +## 서블릿으로 회원 관리 웹 애플리케이션 만들기 + +*MemberFormServlet - 회원 등록 폼* +```java +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form") +public class MemberFormServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + + PrintWriter w = response.getWriter(); + + w.write("\n" + + "\n" + + "\n" + + " \n" + + " Title\n" + + "\n" + + "\n" + + "
\n" + + " username: \n" + + " age: \n" + + " \n" + + "
\n" + + "\n" + + "\n"); } +} +``` + + +*MemberSaveServlet - 회원 저장* +```java +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save") +public class MemberSaveServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("MemberSaveServlet.service"); + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + PrintWriter w = response.getWriter(); + + w.write("\n" + + "\n" + + " \n" + + "\n" + + "\n" + + "성공\n" + + "
    \n" + + "
  • id="+member.getId()+"
  • \n" + + "
  • username="+member.getUsername()+"
  • \n" + + "
  • age="+member.getAge()+"
  • \n" + + "
\n" + + "메인\n" + + "\n" + + ""); + } +} +``` + +- `MemberSaveServlet` 동작 순서 + 1. 파라미터를 조회해 Member 객체 생성 + 2. Member 객체를 MemberRepository를 통해 저장. + 3. Member 객체를 사용해 결과 화면용 HTML을 동적으로 만들어 응답. + + + +*MemberListServlet - 회원 목록* +```java +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members") +public class MemberListServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + List members = memberRepository.findAll(); + + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + + PrintWriter w = response.getWriter(); + w.write(""); + w.write(""); + w.write(" "); + w.write(" Title"); + w.write(""); + w.write(""); + w.write("메인"); + w.write(""); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); +/* + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); +*/ + for (Member member : members) { + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + } + w.write(" "); + w.write("
idusernameage
1userA10
" + member.getId() + "" + member.getUsername() + "" + member.getAge() + "
"); + w.write(""); + w.write(""); + } +} +``` + +- `MemberListServlet` 동작 순서 + 1. `memberRepository.findAll()`을 통해 모든 회원을 조회. + 2. 회원 목록 HTML을 for 루프를 통해 회원 수 만큼 동적으로 생성하고 응답. + + + +### Welcome 페이지 변경 + +*index.html* +```html + + + + + Title + + + + + +``` + + + + +## JSP로 회원 관리 웹 애플리케이션 만들기 + +### JSP 라이브러리 추가 + +*build.gradle* +```groovy + //JSP 추가 시작 +implementation 'org.apache.tomcat.embed:tomcat-embed-jasper' +implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상 +implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //스프링부트3.0 이상 +implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상 +//JSP 추가 끝 +``` + +*new-form.jsp - 회원 등록 폼* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Title + + +
+ username: + age: + +
+ + +``` + +- `<%@ page contentType="text/html;charset=UTF-8" language="java" %>` + - 첫 줄은 JSP 문서라는 뜻. + +*save.jsp - 회원 저장* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="hello.servlet.domain.member.Member" %> +<%@ page import="hello.servlet.domain.member.MemberRepository" %> +<% + MemberRepository memberRepository = MemberRepository.getInstance(); + //request, response는 지원함 + System.out.println("MemberSaveServlet.service"); + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); +%> + + + Title + + + 성공 +
    +
  • id=<%=member.getId()%>
  • +
  • username=<%=member.getUsername()%>
  • +
  • age=<%=member.getAge()%>
  • +
+ 메인 + + +``` + +- '<%@ page import= %>' + - 자바의 import 문과 동일. +- '<% %>' + - 이 부분에 자바 코드 입력 가능. +- `<%= %>` + - 이 부분에 자바 코드 출력 가능. + +회원 저장 JSP는 회원 저장 servlet 코드와 같다. 다른 점은, HTML을 중심으로 하고, +자바 코드를 부분부분 입력해 주었다는 점이다. `<% %>`를 사용해 HTML 중간에 자바 코드를 출력하고 있다. + +*members.jsp - 회원 목록* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="hello.servlet.domain.member.Member" %> +<%@ page import="java.util.List" %> +<%@ page import="hello.servlet.domain.member.MemberRepository" %> +<% + MemberRepository memberRepository = MemberRepository.getInstance(); + + List members = memberRepository.findAll(); +%> + + + + Title + + +메인 + + + + + + + + <% + for (Member member : members) { + out.write(" "); + out.write(" "); + out.write(" "); + out.write(" "); + out.write(" "); + } + %> + +
idusernameage
" + member.getId() + "" + member.getUsername() + "" + member.getAge() + "
+ + +``` + +회원 repository를 먼저 조회, 결과 List를 사용해 중간에 ``HTML 태그를 반복해서 출력하고 있다. + + +### 서블릿과 JSP의 한계 +서블릿으로 개발할 때는 뷰(view)화면을 위한 HTML을 만드는 작업이 자바 코드에 섞여 복잡했다.
+JSP를 사용한 덕분에 뷰를 생성하는 HTML 작업을 깔끔하게 정리하고, 중간중간 동적으로 변경이 필요한 부분에만 +자바 코드를 적용했다. 하지만 몇 가지 문제점이 존재한다. + +회원 저장 폼에서 코드의 상위 절반은 회원을 저장하는 비지니스 로직, 나머지 절반은 결과를 보여주는 HTML 뷰 영역이다. 회원 목록도 마찬가지이다.
+JAVA코드, 데이터를 조회하는 repository 등등 다양한 코드가 모두 JSP에 노출돼 있다. 만약 수백, 수천줄이 넘어간다면 유지보수에 큰 어려움이 발생한다. + +
+**MVC 패턴 등장** +
비지니스 로직은 서블릿 처럼 다른곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면(VIEW)을 보여주는 일에 집중하도록 하게 해준다. + + + + +## MVC 패턴 -적용 +서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해 MVC 패턴 적용 +
Model은 HttpServletRequest 객체 사용. request는 내부에 데이터 저장소를 가지고 있는데, +'request.setAttribute()', `request.getAttribute`를 사용하면 데이터를 보관하고, 조회할 수 있다. + + +### 회원 등록 + +*MvcMemberFormServlet - 회원 등록 폼 컨트롤러* +```java +package hello.servlet.web.servletmvc; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form") +public class MvcMemberFormServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String viewPath = "/WEB-INF/views/new-form.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + + } +} +``` + +`dispatcher.forward()` : 다른 서블릿이나 JSP로 이동할 수 있는 기능. 서버 내부에서 다시 호출 발생 + +> `/WEB-INF`
+> 이 경로 안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 즉 항상 컨트롤러를 통해서 JSP를 호출하는 것이다. + +> **redirect vs forward**
+> 리다이렉트는 실제 클라이언트(web)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다. +> 따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다. 반면에 포워드는 서버 내부에서 +> 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다. + + +*new-form.jsp - 회원 등록 폼 뷰* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + Title + + + +
+ username: + age: + +
+ + +``` + +여기서 form의 action이 상대경로로 지정돼 있다. 이렇게 상대경로로 지정하면 폼 전송시 +현재 URL이 속한 계층 경로 + save가 호출된다.
+현재 계층 경로 : `/servlet-mvc/members/`
+결과 : `/servlet-mvc/members/save` + + +### 회원 저장 + +*MvcMemberSaveServlet - 회원 저장 컨트롤러* +```java +package hello.servlet.web.servletmvc; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save") +public class MvcMemberSaveServlet extends HttpServlet { + + MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + + //Model에 데이터 보관. + request.setAttribute("member", member); + + String viewPath = "/WEB-INF/views/save-result.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} +``` +HttpServletRequest를 Model로 사용.
+request가 제공하는 setAttribute()를 사용하면 request 객체에 데이터를 보관해 뷰에 전달할 수 있다.
+뷰는 request.getAttribute()를 사용해 데이터를 꺼내면 된다. + + +*save-result - 회원저장 뷰* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + + +성공 +
    +
  • id=${member.id}
  • +
  • username=${member.username}
  • +
  • age=${member.age}
  • +
+메인 + + +``` + +JSP는 `${}` 문법을 제공. 이 문법을 사용하면 request의 attribute에 담긴 데이터를 편리하게 조회할 수 있다. + + +### 회원 목록 조회 + +*MvcMemberListServlet - 회원 목록 조회 컨트롤러* +```java +package hello.servlet.web.servletmvc; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members") +public class MvcMemberListServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + + request.setAttribute("members", members); + + String viewPath = "/WEB-INF/views/members.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} +``` + +request 객체를 사용해 `List members`를 모델에 보관한다. + + +*members - 회원 목록 조회 뷰* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> + + + + Title + + +메인 + + + + + + + + + + + + + + + +
idusernameage
${item.id}${item.username}${item.age}
+ + +``` +모델에 담아둔 members를 JSP가 제공하는 taglib기능을 사용해 반복해서 출력한다. +`memebers`리스트에서 `members`를 순서대로 꺼내 `item`변수에 담고, 출력하는 과정을 반복한다. +

+``이 기능을 사용하려면 다음과 같이 선언해야 한다.
+`<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>` + + + +## MVC 패턴 - 한계 +MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다.
+뷰는 화면을 그리는 역할에 충실한 덕분에, 코드가 깔끔하고 직관적이다. 단순하게 모델에서 필요한 데이터를 수집하고, 화면을 만들면 된다. +
+하지만 컨트롤러는 중복코드가 많고, 필요하지 않은 코드가 많이 존재한다. + + +**MVC 컨트롤러의 단점**
+ +*foward 중복*
+view로 이동하는 코드가 항상 중복 호출된다. +``` +RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); +dispatcher.forward(request, response); +``` + + +**ViewPath 중복** +```java +String viewPath = "/WEB-INF/views/save-result.jsp"; +``` + +- prefix : `/WEB-INF/views/` +- suffix : `.jsp`
+그리고 만약 jsp가 아닌 thymeleaf같은 뷰로 변경한다면 전체 코드를 다 변경 작업을 해야 한다. + + +**사용하지 않는 코드**
+다음 코드를 사용할 때도 있고, 사용하지 않을 때도 있다. +``` +HttpServletRequest request, HttpServletResponse response +``` +그리고 이런 `HttpServletRequest`, `HttpServletResponse`를 사용하는 코드는 테스트 케이스를 작성하기 어렵다. + + +**공통 처리 어려움**
+기능이 복잡해질수록 컨트롤러에서 공통으로 처리해야 하는 부분이 점점 더 많이 증가한다. 단순히 공통 기능을 메서드로 +생성하면 될 것 같지만, 결과적으로 해당 메서드를 항상 호출해야 하고, 실수로 호출하지 않으면 문제가 될 수도 있다. 그리고 호출하는 것 자체도 중복이다. + + +**정리!**
+이 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다. 소위 **수문장 역할**을 하는 기능이 필요하다. +**Front Controller**패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다.(입구를 하나로)
+스프링 MVC의 핵심도 바로 이 프론트 컨트롤러에 있다. + + + + + + + + + + + + + + + + + diff --git a/8 WEEK/servlet/SECTION4.md b/8 WEEK/servlet/SECTION4.md new file mode 100644 index 00000000..f27d814e --- /dev/null +++ b/8 WEEK/servlet/SECTION4.md @@ -0,0 +1,1251 @@ +# 4. MVC 프레임워크 만들기 + +--- + +## 프론트 컨트롤러 패턴 + +**프론트 컨트롤러 도입 전** +![S4-1.png](img%2FS4-1.png) +

+**프론트 컨트롤러 도입 후** +![S4-2.png](img%2FS4-2.png) + + +**FrontController 패턴 특징** +- 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음 +- 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아 호출 +- 입구를 하나로 +- 공통 처리 가능 +- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨 + + +**스프링 웹MVC와 프론트 컨트롤러** +- 스프링 웹 MVC의 핵심도 바로 **FrontController** +- 스프링 웹 MVC의 **DispatcherServlet**이 FrontController 패턴으로 구현되어 있다. + + + +## 프론트 컨트롤러 도입 - v1 + +V1 구조 +![S4-3.png](img%2FS4-3.png) + + +*ControllerV1* +```java +package hello.servlet.web.frontcontroller.v1; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface ControllerV1 { + void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; +} +``` + +서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입.
+각 컨트롤러들은 이 인터페이스를 구현
+프론트 컨트롤러는 이 인터페이스를 호출해 구현과 관계 없이 로직의 일관성을 가질 수 있음. + + +*MemberFormControllerV1 - 회원 등록 컨트롤러* +```java +package hello.servlet.web.frontcontroller.v1.controller; + +import hello.servlet.web.frontcontroller.v1.ControllerV1; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberFormControllerV1 implements ControllerV1 { + @Override + public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String viewPath = "/WEB-INF/views/new-form.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} +``` + +*MemberListControllerV1 - 회원 목록 컨트롤러* +```java +package hello.servlet.web.frontcontroller.v1.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.v1.ControllerV1; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +public class MemberListControllerV1 implements ControllerV1 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + + request.setAttribute("members", members); + + String viewPath = "/WEB-INF/views/members.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} +``` +내부 로직은 기존 서블릿과 비슷 + + +*FrontControllerServletV1 - 프론트 컨트롤러* +```java +package hello.servlet.web.frontcontroller.v1; + +import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1; +import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1; +import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*") +public class FrontControllerServiceV1 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV1() { + controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1()); + controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1()); + controllerMap.put("/front-controller/v1/members", new MemberListControllerV1()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV1 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + controller.process(request, response); + } +} +``` + +**프론트 컨트롤러 분석**

+ +**urlPatterns** +- `urlPatterns = "/front-controller/v1/*"` : `/front-controller/v1/*`를 포함한 하위 모든 요청은 +이 서블릿에서 받아들인다. +- Ex) `/front-controller/v1`, `/front-cotroller/v1/a`, `/front-controller/v1/a/b` + + +**controllerMap** +- key : 매핑 URL +- value : 호출된 컨트롤러 + + +**service()**
+먼저 `requestURI`를 조회해 실제 호출할 컨트롤러를 controllerMap에서 찾는다. 없다면 404 상태 코드 반환한다. +
컨트롤러를 찾고 `controller.process(request, response);`을 호출해서 해당 컨트롤러를 실행한다. + + +**JSP**
+JSP는 이전 MVC에서 사용했던 파일을 그대로 사용 +

+ + +**기존 서블릿, JSP로 만든 MVC와 동일하게 실행된다.** + + + + + +## View 분리 - v2 +모든 컨트롤러에서 뷰로 이동한느 부분에 중복 발생, 깔끔하지 않음 +``` + String viewPath = "/WEB-INF/views/members.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); +``` + +이를 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 만든다. + +*V2 구조* +![S4-4.png](img%2FS4-4.png) + +이전 V1 구조에서는 controller에서 jsp를 호출했다면 V2는 view를 반환해 FrontController에서 실행하게 한다. + + +*MyView* +```java +package hello.servlet.web.frontcontroller; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MyView { + private String viewPath; + + public MyView(String viewPath) { + this.viewPath = viewPath; + } + + public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} +``` + +*ControllerV2* +```java +package hello.servlet.web.frontcontroller.v2; + +import hello.servlet.web.frontcontroller.MyView; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface ControllerV2 { + MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; +} +``` + + +*MemberFormControllerV2 - 회원 등록 폼* +```java +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberFormControllerV2 implements ControllerV2 { + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + return new MyView("/WEB-INF/views/new-form.jsp"); + } +} + +``` + +각 컨트롤러는 복잡한 dispatcher.forward()를 직접 생성해 호출하지 않아도 됨. +단순히 MyView 객체를 생성, 거기에 뷰 이름만 넣고 반환하면 됨. + +ControllerV1을 구현한 클래스와 ControllerV2를 구현한 클래스를 비교하면, 중복이 확실하게 제거됨을 확인 가능함 + +*MemberSaveControllerV2 - 회원 저장* +```java +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberSaveControllerV2 implements ControllerV2 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + request.setAttribute("member", member); + return new MyView("/WEB-INF/views/save-result.jsp"); + } +} +``` + + +*MemberListControllerV2 - 회원 목록* +```java +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +public class MemberListControllerV2 implements ControllerV2 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + request.setAttribute("members", members); + return new MyView("/WEB-INF/views/members.jsp"); + } +} +``` + +*FrontControllerV2* +```java +package hello.servlet.web.frontcontroller.v2; + +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*") +public class FrontControllerServiceV2 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV2() { + controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2()); + controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2()); + controllerMap.put("/front-controller/v2/members", new MemberListControllerV2()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV2 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + MyView view = controller.process(request, response); + view.render(request, response); + } +} +``` + +ControllerV2의 반환 타입이 `MyView`이므로 프론트 컨트롤러는 컨트롤러의 호출 결과를 `MyView`를 반환 받는다. +그리고 `view.render()`를 호출하면 `forward` 로직을 수행해 JSP가 실행된다. + +`MyView.render()` +```java + public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +``` + + + + +## Model 추가 - v3 +**서블릿 종속 제거**
+컨트롤러 입장에서 HttpServletRequest, HttpServletResponse가 꼭 필요할까?
+요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 지금 구조에서 컨트롤러가 서블릿 기술을 몰라도 동작 할 수 있다. +또한 request 객체를 Model로 사용하는 대신 별도의 Model 객체를 만들어 반환하면 된다.
+구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않고 변경 ->> + +**뷰 이름 종속 제거**
+컨트롤러에서 지정하는 뷰 이름에 중복이 있는 것을 확인할 수 있다.
+컨트롤러의 *뷰의 논리 이름*을 반환, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화.
+이렇게 하면 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다. + +- `/WEB-INF/views/new-form.jsp` -> **new-form** +- `/WEB-INF/views/save-result.jsp` -> **save-result** +- `/WEB-INF/views/members.jsp` -> **members** + + +**V3 구조** +![S4-5.png](img%2FS4-5.png) + + +**Model View**
+지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다. 그리고 Model도 `request.setAttribute()`를 통해 데이터를 저장, 뷰에 전달했다. +
서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View이름가지 전달하는 객체를 만들어본다. +
참고로 `ModelView`객체는 다른 버전에서도 사용하므로 패키지를 `frontController`에 둔다. + + +*ModelView* +```java +package hello.servlet.web.frontcontroller; + +import java.util.HashMap; +import java.util.Map; +public class ModelView { + private String viewName; + private Map model = new HashMap<>(); + public ModelView(String viewName) { + this.viewName = viewName; + } + public String getViewName() { + return viewName; + } + public void setViewName(String viewName) { + this.viewName = viewName; + } + public Map getModel() { + return model; + } + public void setModel(Map model) { + this.model = model; + } +} +``` +뷰의 이름과 뷰를 렌더링할 때 필요한 model 객체를 가지고 있음. model은 단순히 map으로 되어 있어 컨트롤러에서 뷰에 필요한 데이터를 +key, value로 넣어주면 됨. + + +*ControllerV3* +```java +package hello.servlet.web.frontcontroller.v3; + +import hello.servlet.web.frontcontroller.ModelView; + +import java.util.Map; + +public interface ControllerV3 { + ModelView process(Map paramMap); +} +``` +이 컨트롤러는 서블릿 기술을 사용하지 않음. 따라서 구현이 단순해지고, 테스트 코드를 작성하기 쉬움
+HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아 호출하면 됨
+응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체를 반환하면 됨. + + +*MemberFormControllerV3 - 회원 등록 폼* +```java +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.Map; + +public class MemberFormControllerV3 implements ControllerV3 { + @Override + public ModelView process(Map paramMap) { + return new ModelView("new-form"); + } +} +``` +`ModelView`를 생성할 때 `new-form`이라는 view의 논리적인 이름을 지정. 실제 물리적은 이름은 프론트 컨트롤러에서 처리한다. + + +*MemberSaveControllerV3 - 회원 저장* +```java +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.Map; + +public class MemberSaveControllerV3 implements ControllerV3 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public ModelView process(Map paramMap) { + String username = paramMap.get("username"); + int age = Integer.parseInt(paramMap.get("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + ModelView mv = new ModelView("save-result"); + mv.getModel().put("member", member); + return mv; + } +} +``` + +- `paramMap.get("username");` + - 파라미터 정보는 map에 담겨있음. map에서 필요한 요청 파라미터를 조회하면 됨. +- `mv.getModel().put("member", member);` + - 모델은 단순한 map이므로 모델에 뷰에서 필요한 `member`객체를 담고 반환 + + +*MemberListControllerV3 - 회원 목록* +```java +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.List; +import java.util.Map; + +public class MemberListControllerV3 implements ControllerV3 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public ModelView process(Map paramMap) { + List members = memberRepository.findAll(); + ModelView mv = new ModelView("members"); + mv.getModel().put("members", members); + + return mv; + + } + +} +``` + +*FrontControllerServletV3* +```java +package hello.servlet.web.frontcontroller.v3; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2; +import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*") +public class FrontControllerServiceV3 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV3() { + controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3()); + controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3()); + controllerMap.put("/front-controller/v3/members", new MemberListControllerV3()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV3 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + Map paramMap = createParamMap(request); + ModelView mv = controller.process(paramMap); + String viewName = mv.getViewName(); + MyView view = viewResolver(viewName); + view.render(mv.getModel(), request, response); + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} +``` + + +`view.render(mv.getModel(), request, response)`코드에서 컴파일 오류가 발생함. 다음 코드를 참고해 MyView 객체에 필요한 메서드 추가. + +`createParamMap()`
+HttpServletRequest에서 파라미터 정보를 꺼내 Map으로 변환. 그리고 해당 Map(`paramMap`)을 컨트롤러에 전달하면서 호출 + + +**View Resolver** +`MyView view = viewResolver(viewName)`
+컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다. 그리고 실제 물리 경로가 있는 MyView 객체를 반환 +한다. +- 논리 뷰 이름 : `members` +- 물리 뷰 경로 : `/WEB-INF/views/members.jsp` + + + +`view.render(mv.getModel(), request, response)` +- 뷰 객체를 통해서 HTML 화면을 렌더링 한다. +- 뷰 객체의 render() 는 모델 정보도 함께 받는다. +- JSP는 request.getAttribute() 로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내서 request.setAttribute() 로 담아둔다. +- JSP로 포워드 해서 JSP를 렌더링 한다. + + +*MyView* +```java +package hello.servlet.web.frontcontroller; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Map; + +public class MyView { + private String viewPath; + + public MyView(String viewPath) { + this.viewPath = viewPath; + } + public void render(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } + public void render(Map model, HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + modelToRequestAttribute(model, request); + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } + private void modelToRequestAttribute(Map model, + HttpServletRequest request) { + model.forEach((key, value) -> request.setAttribute(key, value)); + } +} +``` + + + +## 단순하고 실용적인 컨트롤러 - v4 +앞서 만든 v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다. +하지만 실제 컨트롤러 인터페이스를 구현하는 개발자 입장에서 항상 ModelView 객체를 생성하고 반환해야하는 번거러움이 존재한다. +
좋은 프레임 워크는 아키텍쳐도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다. 즉 실용성이 있어야 한다. + + +v4는 v3를 약간 변경해 실제 구현하는 개발자들이 매우 편리하게 개발할 수 있도록 한다. + + +**V4 구조** +![S4-6.png](img%2FS4-6.png) +- 기본적인 구조는 V3와 같다. 대신에 컨트롤러가 ModelView를 반환하지 않고, ViewName만 반환한다. + + +*ControllerV4* +```java +package hello.servlet.web.frontcontroller.v4; + +import java.util.Map; + +public interface ControllerV4 { + String process(Map paramMap, Map model); +} +``` +이번 V4는 interface에 ModelView가 없다. model 객체는 파라미터로 전달되기 때문에 그냥 사용하면 되고, 결과로 뷰의 이름만 반환하면 된다. + + +*MemberFormControllerV4* +```java +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.Map; + +public class MemberFormControllerV4 implements ControllerV4 { + @Override + public String process(Map paramMap, Map model) { + return "new-form"; + } +} +``` + + +*MemberSaveControllerV4* +```java +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.Map; + +public class MemberSaveController implements ControllerV4 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public String process(Map paramMap, Map model) { + String username = paramMap.get("username"); + int age = Integer.parseInt(paramMap.get("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + model.put("member", member); + + return "save-result"; + } +} +``` +`model.put("member", member)` : 모델이 파라미터로 전달되기 때문에 모델을 직접 생성하지 않아도 된다. + + + +*MemberListControllerV4* +```java +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.List; +import java.util.Map; + +public class MemberListControllerV4 implements ControllerV4 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public String process(Map paramMap, Map model) { + List members = memberRepository.findAll(); + + model.put("members", members); + return "members"; + } +} +``` + + +*FrontControllerServletV4* +```java +package hello.servlet.web.frontcontroller.v4; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*") +public class FrontControllerServiceV4 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV4() { + controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4()); + controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4()); + controllerMap.put("/front-controller/v4/members", new MemberListControllerV4()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV4 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + Map paramMap = createParamMap(request); + Map model = new HashMap<>(); + + String viewName = controller.process(paramMap, model); + + MyView view = viewResolver(viewName); + view.render(model, request, response); + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} +``` +`FrontControllerServletV4`는 이전 버전과 거의 동일 +
+ +**모델 객체 전달**
+`Map model = new HashMap<>();`
+모델 객체를 프론트 컨트롤러에서 생성해 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다. + + +**뷰의 논리 이름을 직접 반환**
+```java +String viewName = controller.process(paramMap, model); +MyView view = viewResolver(viewName); +``` +컨트롤러가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해 실제 물리 뷰를 찾을 수 있다. + +**정리** +V4는 매우 단순하고 실용적이다. 기존 구조에서 모델을 파라미터로 넘기고, 뷰의 논리 이름을 반환한다는 작은 아이디어를 적용했을 뿐인데, 컨트롤러를 구현하는 +개발자 입장에서 군더더기 없는 코드를 작성할 수 있다.
+ + + + + +## 유연한 컨트롤러1 - V5 +만약 `ControllerV3` or `ControllerV4` 방식으로 다양한 컨트롤러를 사용해 개발하고 싶다면 **어댑터 패턴을** +을 사용해야 한다. + +**어댑터 패턴** +지금까지 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있었다.
+`ControllerV3`, `ControllerV4`는 완전히 다른 인터페이스이다. 따라서 호환이 불가능하다. 마치 v3는 110v, v4는 220v 전기 콘셉트 같은 것이다. +
어댑터 패턴을 사용해 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경한다. + +**V5 구조** +![S4-7.png](img%2FS4-7.png) +- **핸들러 어댑터** : 중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름은 어댑터 핸들러이다. +여기서 어댑터 역할을 해주기 때문에 다양한 종료의 컨트롤러를 호출할 수 있다. +- **핸들러** : 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 이제 어뎁터가 있기 때문이다.
+꼭 컨트롤러의 개념 뿐 아니라 어떠한 것이든 해당하는 종료의 어댑터만 있으면 다 처리할 수 있기 때문이다. + + +*MyHandlerAdapter* 인터페이스 +```java +package hello.servlet.web.frontcontroller.v5; + +import hello.servlet.web.frontcontroller.ModelView; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface MyHandlerAdapter { + boolean supports(Object handler); + + ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException; +} +``` +- `boolean supports(Object handler)` + - handler는 컨트롤러를 말함 + - 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 Method. +- `ModelView handle(HttpServletRequest request, HttpServletResponse response, Object Handler)` + - 어댑터는 실제 컨트롤러를 호출, 그 결과로 ModelView를 반환해야 함. + - 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 함. + - 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만, 이제는 이 어댑터를 통해 실제 컨트롤러가 호출됨. + + +실제 ControllerV3를 지원하는 어댑터를 구현
+*ControllerV3HandlerAdapter* +```java +package hello.servlet.web.frontcontroller.v5.adapter; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; +import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; +public class ControllerV3HandlerAdapter implements MyHandlerAdapter { + @Override + public boolean supports(Object handler) { + return (handler instanceof ControllerV3); + } + @Override + public ModelView handle(HttpServletRequest request, HttpServletResponse + response, Object handler) { + ControllerV3 controller = (ControllerV3) handler; + Map paramMap = createParamMap(request); + ModelView mv = controller.process(paramMap); + return mv; + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } +} +``` + +

+ +```java +public boolean supports(Object handler) { + return (handler instanceof ControllerV3); +} +``` +`ControllerV3`를 처리할 수 있는 어댑터. + + +```java +public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) { + ControllerV3 controller = (ControllerV3) handler; + Map paramMap = createParamMap(request); + ModelView mv = controller.process(paramMap); + return mv; +} +``` +handler를 컨트롤러 V3로 변환한 다음 V3 형식에 맞추도록 호출.
+`support()`를 통해 `ControllerV3`만 지원하기 때문에 타입 변환은 걱정없이 실행해도 됨.
+ControllerV3는 ModelView를 반환하므로 그대로 ModelView를 반환하면 됨. + + + +*FrontControllerServletV5* +```java +package hello.servlet.web.frontcontroller.v5; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3; +import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/ v5/*") +public class FrontControllerServletV5 extends HttpServlet { + private final Map handlerMappingMap = new HashMap<>(); + private final List handlerAdapters = new ArrayList<>(); + public FrontControllerServletV5() { + initHandlerMappingMap(); + initHandlerAdapters(); + } + private void initHandlerMappingMap() { + handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3()); + } + private void initHandlerAdapters() { + handlerAdapters.add(new ControllerV3HandlerAdapter()); + } + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + Object handler = getHandler(request); + if (handler == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + MyHandlerAdapter adapter = getHandlerAdapter(handler); + ModelView mv = adapter.handle(request, response, handler); + MyView view = viewResolver(mv.getViewName()); + view.render(mv.getModel(), request, response); + } + private Object getHandler(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return handlerMappingMap.get(requestURI); + } + private MyHandlerAdapter getHandlerAdapter(Object handler) { + for (MyHandlerAdapter adapter : handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } + } + throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler); + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} +``` + +**컨트롤러(Controller) -> 핸들러(Handler)**
+이전에는 컨트롤러를 직접 매핑해서 사용했다. 이젠느 어댑터를 사용하기 때문에, 컨트롤러 뿐 아니라 어댑터가 지원하기만 하면, +어떤 것이라도 URL에 매핑해 사용할 수 있다. 그래서 이름을 컨트롤러에서 더 넓은 범위의 핸들러로 변경했다. + + +**생성자** +```java + public FrontControllerServletV5() { + initHandlerMappingMap(); + initHandlerAdapters(); + } +``` +생성자는 핸들러 매핑과 어댑터를 초기화(등록)함 + + +**매핑 정보**
+`private final Map handlerMappingMap = new HashMap<>();`
+ +매핑 정보의 값이 ControllerV3 , ControllerV4 같은 인터페이스에서 아무 값이나 받을 수 있는 Object 로 변 +경되었다 + + +**핸들러 매핑**
+`Object handler = getHandler(request);` +```java + private Object getHandler(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return handlerMappingMap.get(requestURI); + } +``` +핸들러 매핑 정보인 `handlerMappingMap`에서 URL에 매핑된 핸들러(컨트롤러)객체를 찾아 반환 + +

+ +**핸들러를 처리할 수 있는 어댑터 조회**
+`MyHandlerAdapter adapter = getHandlerAdapter(handler)` +``` +for (MyHandlerAdapter adapter : handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } +} +``` + +`handler`를 처리활 수 있는 어댑터를 `adapter.supports(handler)`를 통해 찾음.
+handler가 `ControllerV3` 인터페이스를 구현했다면, `ControllerV3HandlerAdapter()`객체가 반환됨. + + +**어댑터 호출**
+`ModelView mv = adapter.handler(request, response, handler);` + +어댑터의 `handler(request, response, handler)`메서드를 통해 실제 어댑터가 호출됨.
+어댑터는 handler(컨트롤러)를 호출하고 그 결과를 어댑터에 맞추어 반환. +`ControllerV3HandlerAdapter`의 경우 어댑터의 모양과 컨트롤러의 모양이 유사해 변환 로직이 단순함 + + + +## 유연한 컨트롤러2 - v5 + +*FrontControllerServletV5* - `Controller4` 기능 추가 +```java +package hello.servlet.web.frontcontroller.v5; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3; +import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4; +import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter; +import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*") +public class FrontControllerServletV5 extends HttpServlet { + private final Map handlerMappingMap = new HashMap<>(); + private final List handlerAdapters = new ArrayList<>(); + public FrontControllerServletV5() { + initHandlerMappingMap(); + initHandlerAdapters(); + } + private void initHandlerMappingMap() { + handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3()); + + handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4()); + handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4()); + handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4()); + } + + private void initHandlerAdapters() { + handlerAdapters.add(new ControllerV3HandlerAdapter()); + handlerAdapters.add(new ControllerV4HandlerAdapter()); + } + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + Object handler = getHandler(request); + if (handler == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + MyHandlerAdapter adapter = getHandlerAdapter(handler); + ModelView mv = adapter.handle(request, response, handler); + MyView view = viewResolver(mv.getViewName()); + view.render(mv.getModel(), request, response); + } + private Object getHandler(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return handlerMappingMap.get(requestURI); + } + private MyHandlerAdapter getHandlerAdapter(Object handler) { + for (MyHandlerAdapter adapter : handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } + } + throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler); + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} +``` + +`handlerMappingMap`에 `ControllerV4`를 사용하는 컨트롤러를 추가, 해당 컨트롤러를 처리할 수 있는 어댑터인 `ControllerV4HandlerAdapter`도 추가 + + +*ControllerV4HandlerAdapter* +```java +package hello.servlet.web.frontcontroller.v5.adapter; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v4.ControllerV4; +import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class ControllerV4HandlerAdapter implements MyHandlerAdapter { + @Override + public boolean supports(Object handler) { + return (handler instanceof ControllerV4); + } + + @Override + public ModelView handle(HttpServletRequest request, HttpServletResponse + response, Object handler) { + ControllerV4 controller = (ControllerV4) handler; + Map paramMap = createParamMap(request); + Map model = new HashMap<>(); + String viewName = controller.process(paramMap, model); + + ModelView mv = new ModelView(viewName); + mv.setModel(model); + + return mv; + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } +} +``` + +분석 +```java + public boolean supports(Object handler) { + return (handler instanceof ControllerV4); + } +``` +`handler`가 `ControllerV4`인 경우에만 처리하는 어댑터. + +실행로직 +```java + ControllerV4 controller = (ControllerV4) handler; + Map paramMap = createParamMap(request); + Map model = new HashMap<>(); + String viewName = controller.process(paramMap, model); +``` +handler를 ControllerV4로 케스팅, paramMap, model을 만들어 해당 컨트롤러를 호출한다. +그리고 viewName을 반환 받는다. + + +**어댑터 반환** +``` + ModelView mv = new ModelView(viewName); + mv.setModel(model); + + return mv; +``` +어댑터에서 이 부분이 중요한 부분임 + +어댑터가 호출하는 `ControllerV4`는 뷰의 이름알 반환. 그런데 어댑터는 뷰의 이름이 아니라 `ModelView`를 만들어 반환해야함 +여기서 어댑터가 꼭 필요한 이유가 나옴.
+`ControllerV4`는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식을 맞춰 반환함. +마치 110v 콘센트를 220v 콘센트로 변경하듯. + + +**ControllerV4 & Adapter** +```java +public interface ControllerV4 { + String process(Map paramMap, Map model); +} +public interface MyHandlerAdapter { + ModelView handle(HttpServletRequest request, HttpServletResponse response, +Object handler) throws ServletException, IOException; +} +``` + + + + +--- \ No newline at end of file diff --git a/8 WEEK/servlet/build.gradle b/8 WEEK/servlet/build.gradle new file mode 100644 index 00000000..de959ed2 --- /dev/null +++ b/8 WEEK/servlet/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'java' + id 'war' + id 'org.springframework.boot' version '3.2.1' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'hello' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + + //JSP 추가 시작 + implementation 'org.apache.tomcat.embed:tomcat-embed-jasper' + implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상 + implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //스프링부트3.0 이상 + implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상 + //JSP 추가 끝 + + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar b/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar differ diff --git a/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties b/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1af9e093 --- /dev/null +++ b/8 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/8 WEEK/servlet/gradlew b/8 WEEK/servlet/gradlew new file mode 100644 index 00000000..1aa94a42 --- /dev/null +++ b/8 WEEK/servlet/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/8 WEEK/servlet/gradlew.bat b/8 WEEK/servlet/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/8 WEEK/servlet/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/8 WEEK/servlet/img/S1-1.png b/8 WEEK/servlet/img/S1-1.png new file mode 100644 index 00000000..b4382b9b Binary files /dev/null and b/8 WEEK/servlet/img/S1-1.png differ diff --git a/8 WEEK/servlet/img/S1-2.png b/8 WEEK/servlet/img/S1-2.png new file mode 100644 index 00000000..ff31b6a6 Binary files /dev/null and b/8 WEEK/servlet/img/S1-2.png differ diff --git a/8 WEEK/servlet/img/S1-3.png b/8 WEEK/servlet/img/S1-3.png new file mode 100644 index 00000000..d0a945c4 Binary files /dev/null and b/8 WEEK/servlet/img/S1-3.png differ diff --git a/8 WEEK/servlet/img/S1-4.png b/8 WEEK/servlet/img/S1-4.png new file mode 100644 index 00000000..b2538fe4 Binary files /dev/null and b/8 WEEK/servlet/img/S1-4.png differ diff --git a/8 WEEK/servlet/img/S1-5.png b/8 WEEK/servlet/img/S1-5.png new file mode 100644 index 00000000..496dd994 Binary files /dev/null and b/8 WEEK/servlet/img/S1-5.png differ diff --git a/8 WEEK/servlet/img/S1-6.png b/8 WEEK/servlet/img/S1-6.png new file mode 100644 index 00000000..0f147271 Binary files /dev/null and b/8 WEEK/servlet/img/S1-6.png differ diff --git a/8 WEEK/servlet/img/S1-7.png b/8 WEEK/servlet/img/S1-7.png new file mode 100644 index 00000000..cc6ffda9 Binary files /dev/null and b/8 WEEK/servlet/img/S1-7.png differ diff --git a/8 WEEK/servlet/img/S1-8.png b/8 WEEK/servlet/img/S1-8.png new file mode 100644 index 00000000..dafb1dc7 Binary files /dev/null and b/8 WEEK/servlet/img/S1-8.png differ diff --git a/8 WEEK/servlet/img/S2-1.png b/8 WEEK/servlet/img/S2-1.png new file mode 100644 index 00000000..2f468be2 Binary files /dev/null and b/8 WEEK/servlet/img/S2-1.png differ diff --git a/8 WEEK/servlet/img/S2-2.png b/8 WEEK/servlet/img/S2-2.png new file mode 100644 index 00000000..c2d94f77 Binary files /dev/null and b/8 WEEK/servlet/img/S2-2.png differ diff --git a/8 WEEK/servlet/img/S2-3.png b/8 WEEK/servlet/img/S2-3.png new file mode 100644 index 00000000..15d29abf Binary files /dev/null and b/8 WEEK/servlet/img/S2-3.png differ diff --git a/8 WEEK/servlet/img/S2-4.png b/8 WEEK/servlet/img/S2-4.png new file mode 100644 index 00000000..a6ce3e89 Binary files /dev/null and b/8 WEEK/servlet/img/S2-4.png differ diff --git a/8 WEEK/servlet/img/S2-5.png b/8 WEEK/servlet/img/S2-5.png new file mode 100644 index 00000000..5c762ffa Binary files /dev/null and b/8 WEEK/servlet/img/S2-5.png differ diff --git a/8 WEEK/servlet/img/S2-6.png b/8 WEEK/servlet/img/S2-6.png new file mode 100644 index 00000000..c5924ca5 Binary files /dev/null and b/8 WEEK/servlet/img/S2-6.png differ diff --git a/8 WEEK/servlet/img/S2-7.png b/8 WEEK/servlet/img/S2-7.png new file mode 100644 index 00000000..3948838e Binary files /dev/null and b/8 WEEK/servlet/img/S2-7.png differ diff --git a/8 WEEK/servlet/img/S2-8.png b/8 WEEK/servlet/img/S2-8.png new file mode 100644 index 00000000..76f40bbd Binary files /dev/null and b/8 WEEK/servlet/img/S2-8.png differ diff --git a/8 WEEK/servlet/img/S4-1.png b/8 WEEK/servlet/img/S4-1.png new file mode 100644 index 00000000..3944b4aa Binary files /dev/null and b/8 WEEK/servlet/img/S4-1.png differ diff --git a/8 WEEK/servlet/img/S4-2.png b/8 WEEK/servlet/img/S4-2.png new file mode 100644 index 00000000..6f55a519 Binary files /dev/null and b/8 WEEK/servlet/img/S4-2.png differ diff --git a/8 WEEK/servlet/img/S4-3.png b/8 WEEK/servlet/img/S4-3.png new file mode 100644 index 00000000..d907f0d0 Binary files /dev/null and b/8 WEEK/servlet/img/S4-3.png differ diff --git a/8 WEEK/servlet/img/S4-4.png b/8 WEEK/servlet/img/S4-4.png new file mode 100644 index 00000000..bd12948f Binary files /dev/null and b/8 WEEK/servlet/img/S4-4.png differ diff --git a/8 WEEK/servlet/img/S4-5.png b/8 WEEK/servlet/img/S4-5.png new file mode 100644 index 00000000..beb9c402 Binary files /dev/null and b/8 WEEK/servlet/img/S4-5.png differ diff --git a/8 WEEK/servlet/img/S4-6.png b/8 WEEK/servlet/img/S4-6.png new file mode 100644 index 00000000..f2539420 Binary files /dev/null and b/8 WEEK/servlet/img/S4-6.png differ diff --git a/8 WEEK/servlet/img/S4-7.png b/8 WEEK/servlet/img/S4-7.png new file mode 100644 index 00000000..c805b5b0 Binary files /dev/null and b/8 WEEK/servlet/img/S4-7.png differ diff --git a/8 WEEK/servlet/settings.gradle b/8 WEEK/servlet/settings.gradle new file mode 100644 index 00000000..9c5e4d1a --- /dev/null +++ b/8 WEEK/servlet/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'servlet' diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java b/8 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java new file mode 100644 index 00000000..a1c772de --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java @@ -0,0 +1,15 @@ +package hello.servlet; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.ServletComponentScan; + +@ServletComponentScan //서블릿 자동 등록 +@SpringBootApplication +public class ServletApplication { + + public static void main(String[] args) { + SpringApplication.run(ServletApplication.class, args); + } + +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java b/8 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java new file mode 100644 index 00000000..057e157f --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java @@ -0,0 +1,13 @@ +package hello.servlet; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +public class ServletInitializer extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(ServletApplication.class); + } + +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java new file mode 100644 index 00000000..c514bc33 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java @@ -0,0 +1,11 @@ +package hello.servlet.basic; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class HelloData { + private String username; + private int age; +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java new file mode 100644 index 00000000..4a128f82 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java @@ -0,0 +1,26 @@ +package hello.servlet.basic; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "helloServlet", urlPatterns = "/hello") +public class HelloServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException { + System.out.println("HelloServlet.service"); + System.out.println("requset = " + requset); + System.out.println("response = " + response); + + String username = requset.getParameter("username"); + System.out.println("username = " + username); + + response.setContentType("text/plain"); + response.setCharacterEncoding("utf-8"); + response.getWriter().write("hello " + username); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java new file mode 100644 index 00000000..3bb95632 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java @@ -0,0 +1,35 @@ +package hello.servlet.basic.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import hello.servlet.basic.HelloData; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json") +public class RequestBodyJsonServlet extends HttpServlet { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + + System.out.println("messageBody = " + messageBody); + + HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); + + System.out.println("helloData.username = " + helloData.getUsername()); + System.out.println("helloData.age = " + helloData.getAge()); + + response.getWriter().write("ok"); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java new file mode 100644 index 00000000..660342ba --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java @@ -0,0 +1,25 @@ +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@WebServlet(name="requestBodyStringServlet", urlPatterns = "/request-body-string") +public class RequestBodyStringServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + + System.out.println("messageBody = " + messageBody); + + response.getWriter().write("OK"); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java new file mode 100644 index 00000000..3d19efca --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java @@ -0,0 +1,108 @@ +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header") +public class RequestHeaderServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + printStartLine(request); + printHeaders(request); + printHeaderUtils(request); + printEtc(request); + + } + + //start line 정보 + private void printStartLine(HttpServletRequest request) { + System.out.println("--- REQUEST-LINE - start ---"); + System.out.println("request.getMethod() = " + request.getMethod()); //GET + System.out.println("request.getProtocol() = " + request.getProtocol()); //HTTP/1.1 + System.out.println("request.getScheme() = " + request.getScheme()); //http + // http://localhost:8080/request-header + System.out.println("request.getRequestURL() = " + request.getRequestURL()); + // /request-header + System.out.println("request.getRequestURI() = " + request.getRequestURI()); + //username=hi + System.out.println("request.getQueryString() = " + + request.getQueryString()); + System.out.println("request.isSecure() = " + request.isSecure()); //https 사용유무 + System.out.println("--- REQUEST-LINE - end ---"); + System.out.println(); + } + + //Header 모든 정보 + private void printHeaders(HttpServletRequest request) { + System.out.println("--- Headers - start ---"); + /* + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + System.out.println(headerName + ": " + request.getHeader(headerName)); + } + */ + request.getHeaderNames().asIterator() + .forEachRemaining(headerName -> System.out.println(headerName + ": " + + request.getHeader(headerName))); + System.out.println("--- Headers - end ---"); + System.out.println(); + } + + + //Header 편리한 조회 + private void printHeaderUtils(HttpServletRequest request) { + System.out.println("--- Header 편의 조회 start ---"); + System.out.println("[Host 편의 조회]"); + System.out.println("request.getServerName() = " + + request.getServerName()); //Host 헤더 + System.out.println("request.getServerPort() = " + + request.getServerPort()); //Host 헤더 + System.out.println(); + System.out.println("[Accept-Language 편의 조회]"); + request.getLocales().asIterator() + .forEachRemaining(locale -> System.out.println("locale = " + + locale)); + System.out.println("request.getLocale() = " + request.getLocale()); + System.out.println(); + System.out.println("[cookie 편의 조회]"); + if (request.getCookies() != null) { + for (Cookie cookie : request.getCookies()) { + System.out.println(cookie.getName() + ": " + cookie.getValue()); + } + } + System.out.println(); + System.out.println("[Content 편의 조회]"); + System.out.println("request.getContentType() = " + + request.getContentType()); + System.out.println("request.getContentLength() = " + + request.getContentLength()); + System.out.println("request.getCharacterEncoding() = " + + request.getCharacterEncoding()); + } + + //기타 정보 + private void printEtc(HttpServletRequest request) { + System.out.println("--- 기타 조회 start ---"); + System.out.println("[Remote 정보]"); + System.out.println("request.getRemoteHost() = " + + request.getRemoteHost()); // + System.out.println("request.getRemoteAddr() = " + + request.getRemoteAddr()); // + System.out.println("request.getRemotePort() = " + + request.getRemotePort()); // + System.out.println(); + System.out.println("[Local 정보]"); + System.out.println("request.getLocalName() = " + request.getLocalName()); // + System.out.println("request.getLocalAddr() = " + request.getLocalAddr()); // + System.out.println("request.getLocalPort() = " + request.getLocalPort()); // + System.out.println("--- 기타 조회 end ---"); + System.out.println(); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java new file mode 100644 index 00000000..d90e08d1 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java @@ -0,0 +1,39 @@ +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Enumeration; + +@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param") +public class RequestParamServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException { + System.out.println("[전체 파라미터 조회] - start"); + +// Enumeration parameterNames = requset.getParameterNames(); + requset.getParameterNames().asIterator().forEachRemaining( + paramName -> System.out.println(paramName + "=" + requset.getParameter(paramName)) + ); + + System.out.println("[전체 파라미터 조회] - end"); + System.out.println(); + + System.out.println("[단일 파라미터 조회]"); + String username= requset.getParameter("username"); + String age = requset.getParameter("age"); + + System.out.println("username = " + username); + System.out.println("age = " + age); + + System.out.println("[이름이 같은 복수 파라미터 조회]"); + String[] usernames = requset.getParameterValues("username"); + for(String name: usernames) { + System.out.println("username = " + name); + } + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java new file mode 100644 index 00000000..12923c0a --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java @@ -0,0 +1,62 @@ +package hello.servlet.basic.response; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header") +public class ResponseHeaderServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + //[status-line] + response.setStatus(HttpServletResponse.SC_OK); + + //[response-headers] + response.setHeader("Content-Type", "text/plain;charset=utf-8"); + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("my-header","hello"); + + //[Header 편의 메서드] +// content(response); +// cookie(response); +// redirect(response); + + //[message body] + PrintWriter writer = response.getWriter(); + writer.println("ok"); + } + + private void content(HttpServletResponse response) { + //Content-Type: text/plain;charset=utf-8 + //Content-Length: 2 + //response.setHeader("Content-Type", "text/plain;charset=utf-8"); + response.setContentType("text/plain"); + response.setCharacterEncoding("utf-8"); + //response.setContentLength(2); //(생략시 자동 생성) + } + + private void cookie(HttpServletResponse response) { + //Set-Cookie: myCookie=good; Max-Age=600; + //response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600"); + Cookie cookie = new Cookie("myCookie", "good"); + cookie.setMaxAge(600); //600초 + response.addCookie(cookie); + } + + private void redirect(HttpServletResponse response) throws IOException { + //Status Code 302 + //Location: /basic/hello-form.html + //response.setStatus(HttpServletResponse.SC_FOUND); //302 + //response.setHeader("Location", "/basic/hello-form.html"); + response.sendRedirect("/basic/hello-form.html"); + } + +} + diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java new file mode 100644 index 00000000..a64c8450 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java @@ -0,0 +1,27 @@ +package hello.servlet.basic.response; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html") +public class ResponseHtmlServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + //Content-Type: text/html;charset=utf-8 + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + PrintWriter writer = response.getWriter(); + writer.println(""); + writer.println(""); + writer.println("
HI?
"); + writer.println(""); + writer.println(""); + } +} + diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java new file mode 100644 index 00000000..5260635b --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java @@ -0,0 +1,30 @@ +package hello.servlet.basic.response; + +import com.fasterxml.jackson.databind.ObjectMapper; +import hello.servlet.basic.HelloData; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json") +public class ResponseJsonServlet extends HttpServlet { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + + HelloData helloData = new HelloData(); + helloData.setUsername("lee"); + helloData.setAge(20); + + String result = objectMapper.writeValueAsString(helloData); + response.getWriter().write(result); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java b/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java new file mode 100644 index 00000000..6fe75506 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java @@ -0,0 +1,18 @@ +package hello.servlet.domain.member; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Member { + + private Long id; + private String username; + private int age; + + public Member(String username, int age) { + this.username = username; + this.age = age; + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java b/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java new file mode 100644 index 00000000..15eaaf81 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java @@ -0,0 +1,38 @@ +package hello.servlet.domain.member; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MemberRepository { + + private static Map store = new HashMap<>(); + private static long sequence = 0L; + + //싱글톤 + private static final MemberRepository instance = new MemberRepository(); + + public static MemberRepository getInstance() { + return instance; + } + private MemberRepository(){} + + public Member save(Member member) { + member.setId(++sequence); + store.put(member.getId(), member); + return member; + } + + public Member findById(Long id) { + return store.get(id); + } + + public List findAll() { + return new ArrayList<>(store.values()); + } + + public void clearStore() { + store.clear(); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java new file mode 100644 index 00000000..a6a33309 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java @@ -0,0 +1,23 @@ +package hello.servlet.web.frontcontroller; + +import java.util.HashMap; +import java.util.Map; +public class ModelView { + private String viewName; + private Map model = new HashMap<>(); + public ModelView(String viewName) { + this.viewName = viewName; + } + public String getViewName() { + return viewName; + } + public void setViewName(String viewName) { + this.viewName = viewName; + } + public Map getModel() { + return model; + } + public void setModel(Map model) { + this.model = model; + } +} \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java new file mode 100644 index 00000000..4da0c980 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java @@ -0,0 +1,32 @@ +package hello.servlet.web.frontcontroller; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Map; + +public class MyView { + private String viewPath; + + public MyView(String viewPath) { + this.viewPath = viewPath; + } + public void render(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } + public void render(Map model, HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + modelToRequestAttribute(model, request); + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } + private void modelToRequestAttribute(Map model, + HttpServletRequest request) { + model.forEach((key, value) -> request.setAttribute(key, value)); + } +} \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java new file mode 100644 index 00000000..0e2f00a3 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java @@ -0,0 +1,11 @@ +package hello.servlet.web.frontcontroller.v1; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface ControllerV1 { + void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java new file mode 100644 index 00000000..6dc0cab1 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java @@ -0,0 +1,40 @@ +package hello.servlet.web.frontcontroller.v1; + +import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1; +import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1; +import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*") +public class FrontControllerServiceV1 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV1() { + controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1()); + controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1()); + controllerMap.put("/front-controller/v1/members", new MemberListControllerV1()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV1 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + controller.process(request, response); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java new file mode 100644 index 00000000..e4927c67 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java @@ -0,0 +1,18 @@ +package hello.servlet.web.frontcontroller.v1.controller; + +import hello.servlet.web.frontcontroller.v1.ControllerV1; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberFormControllerV1 implements ControllerV1 { + @Override + public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String viewPath = "/WEB-INF/views/new-form.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java new file mode 100644 index 00000000..da67d7b2 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java @@ -0,0 +1,28 @@ +package hello.servlet.web.frontcontroller.v1.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.v1.ControllerV1; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +public class MemberListControllerV1 implements ControllerV1 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + + request.setAttribute("members", members); + + String viewPath = "/WEB-INF/views/members.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java new file mode 100644 index 00000000..c089740b --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java @@ -0,0 +1,30 @@ +package hello.servlet.web.frontcontroller.v1.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.v1.ControllerV1; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberSaveControllerV1 implements ControllerV1 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + request.setAttribute("member", member); + + String viewPath = "/WEB-INF/views/save-result.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java new file mode 100644 index 00000000..6e1de8a2 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java @@ -0,0 +1,12 @@ +package hello.servlet.web.frontcontroller.v2; + +import hello.servlet.web.frontcontroller.MyView; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface ControllerV2 { + MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java new file mode 100644 index 00000000..8a46b0c9 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java @@ -0,0 +1,42 @@ +package hello.servlet.web.frontcontroller.v2; + +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*") +public class FrontControllerServiceV2 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV2() { + controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2()); + controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2()); + controllerMap.put("/front-controller/v2/members", new MemberListControllerV2()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV2 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + MyView view = controller.process(request, response); + view.render(request, response); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java new file mode 100644 index 00000000..ffdece7a --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java @@ -0,0 +1,16 @@ +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberFormControllerV2 implements ControllerV2 { + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + return new MyView("/WEB-INF/views/new-form.jsp"); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java new file mode 100644 index 00000000..1a69f49e --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java @@ -0,0 +1,24 @@ +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +public class MemberListControllerV2 implements ControllerV2 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + request.setAttribute("members", members); + return new MyView("/WEB-INF/views/members.jsp"); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java new file mode 100644 index 00000000..e56b5984 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java @@ -0,0 +1,28 @@ +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberSaveControllerV2 implements ControllerV2 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + request.setAttribute("member", member); + return new MyView("/WEB-INF/views/save-result.jsp"); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java new file mode 100644 index 00000000..ba2edc59 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java @@ -0,0 +1,9 @@ +package hello.servlet.web.frontcontroller.v3; + +import hello.servlet.web.frontcontroller.ModelView; + +import java.util.Map; + +public interface ControllerV3 { + ModelView process(Map paramMap); +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java new file mode 100644 index 00000000..46e92e6d --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java @@ -0,0 +1,56 @@ +package hello.servlet.web.frontcontroller.v3; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*") +public class FrontControllerServiceV3 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV3() { + controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3()); + controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3()); + controllerMap.put("/front-controller/v3/members", new MemberListControllerV3()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV3 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + Map paramMap = createParamMap(request); + ModelView mv = controller.process(paramMap); + String viewName = mv.getViewName(); + MyView view = viewResolver(viewName); + view.render(mv.getModel(), request, response); + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java new file mode 100644 index 00000000..4b886d83 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java @@ -0,0 +1,13 @@ +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.Map; + +public class MemberFormControllerV3 implements ControllerV3 { + @Override + public ModelView process(Map paramMap) { + return new ModelView("new-form"); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java new file mode 100644 index 00000000..897cb7fd --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java @@ -0,0 +1,24 @@ +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.List; +import java.util.Map; + +public class MemberListControllerV3 implements ControllerV3 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public ModelView process(Map paramMap) { + List members = memberRepository.findAll(); + ModelView mv = new ModelView("members"); + mv.getModel().put("members", members); + + return mv; + + } + +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java new file mode 100644 index 00000000..cae1ba01 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java @@ -0,0 +1,25 @@ +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.Map; + +public class MemberSaveControllerV3 implements ControllerV3 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public ModelView process(Map paramMap) { + String username = paramMap.get("username"); + int age = Integer.parseInt(paramMap.get("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + ModelView mv = new ModelView("save-result"); + mv.getModel().put("member", member); + return mv; + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java new file mode 100644 index 00000000..6a4a5803 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java @@ -0,0 +1,7 @@ +package hello.servlet.web.frontcontroller.v4; + +import java.util.Map; + +public interface ControllerV4 { + String process(Map paramMap, Map model); +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java new file mode 100644 index 00000000..065a4e8f --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java @@ -0,0 +1,58 @@ +package hello.servlet.web.frontcontroller.v4; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*") +public class FrontControllerServiceV4 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV4() { + controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4()); + controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4()); + controllerMap.put("/front-controller/v4/members", new MemberListControllerV4()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV4 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + Map paramMap = createParamMap(request); + Map model = new HashMap<>(); + + String viewName = controller.process(paramMap, model); + + MyView view = viewResolver(viewName); + view.render(model, request, response); + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberFormControllerV4.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberFormControllerV4.java new file mode 100644 index 00000000..288bdff0 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberFormControllerV4.java @@ -0,0 +1,12 @@ +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.Map; + +public class MemberFormControllerV4 implements ControllerV4 { + @Override + public String process(Map paramMap, Map model) { + return "new-form"; + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberListControllerV4.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberListControllerV4.java new file mode 100644 index 00000000..e5a43cfc --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberListControllerV4.java @@ -0,0 +1,22 @@ +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.List; +import java.util.Map; + +public class MemberListControllerV4 implements ControllerV4 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public String process(Map paramMap, Map model) { + List members = memberRepository.findAll(); + + model.put("members", members); + return "members"; + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerV4.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerV4.java new file mode 100644 index 00000000..2a4c1128 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerV4.java @@ -0,0 +1,24 @@ +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.Map; + +public class MemberSaveControllerV4 implements ControllerV4 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public String process(Map paramMap, Map model) { + String username = paramMap.get("username"); + int age = Integer.parseInt(paramMap.get("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + model.put("member", member); + + return "save-result"; + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5.java new file mode 100644 index 00000000..778e60db --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5.java @@ -0,0 +1,71 @@ +package hello.servlet.web.frontcontroller.v5; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3; +import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4; +import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter; +import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*") +public class FrontControllerServletV5 extends HttpServlet { + private final Map handlerMappingMap = new HashMap<>(); + private final List handlerAdapters = new ArrayList<>(); + public FrontControllerServletV5() { + initHandlerMappingMap(); + initHandlerAdapters(); + } + private void initHandlerMappingMap() { + handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3()); + + handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4()); + handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4()); + handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4()); + } + + private void initHandlerAdapters() { + handlerAdapters.add(new ControllerV3HandlerAdapter()); + handlerAdapters.add(new ControllerV4HandlerAdapter()); + } + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + Object handler = getHandler(request); + if (handler == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + MyHandlerAdapter adapter = getHandlerAdapter(handler); + ModelView mv = adapter.handle(request, response, handler); + MyView view = viewResolver(mv.getViewName()); + view.render(mv.getModel(), request, response); + } + private Object getHandler(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return handlerMappingMap.get(requestURI); + } + private MyHandlerAdapter getHandlerAdapter(Object handler) { + for (MyHandlerAdapter adapter : handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } + } + throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler); + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/MyHandlerAdapter.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/MyHandlerAdapter.java new file mode 100644 index 00000000..db175c53 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/MyHandlerAdapter.java @@ -0,0 +1,14 @@ +package hello.servlet.web.frontcontroller.v5; + +import hello.servlet.web.frontcontroller.ModelView; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface MyHandlerAdapter { + boolean supports(Object handler); + + ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException; +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter.java new file mode 100644 index 00000000..5453bbfa --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter.java @@ -0,0 +1,29 @@ +package hello.servlet.web.frontcontroller.v5.adapter; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; +import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; +public class ControllerV3HandlerAdapter implements MyHandlerAdapter { + @Override + public boolean supports(Object handler) { + return (handler instanceof ControllerV3); + } + @Override + public ModelView handle(HttpServletRequest request, HttpServletResponse + response, Object handler) { + ControllerV3 controller = (ControllerV3) handler; + Map paramMap = createParamMap(request); + ModelView mv = controller.process(paramMap); + return mv; + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } +} \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter.java new file mode 100644 index 00000000..99b967a9 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter.java @@ -0,0 +1,40 @@ +package hello.servlet.web.frontcontroller.v5.adapter; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v4.ControllerV4; +import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class ControllerV4HandlerAdapter implements MyHandlerAdapter { + @Override + public boolean supports(Object handler) { + return (handler instanceof ControllerV4); + } + + @Override + public ModelView handle(HttpServletRequest request, HttpServletResponse + response, Object handler) { + ControllerV4 controller = (ControllerV4) handler; + Map paramMap = createParamMap(request); + Map model = new HashMap<>(); + String viewName = controller.process(paramMap, model); + + ModelView mv = new ModelView(viewName); + mv.setModel(model); + + return mv; + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } +} \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberFormServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberFormServlet.java new file mode 100644 index 00000000..8c127920 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberFormServlet.java @@ -0,0 +1,38 @@ +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form") +public class MemberFormServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + + PrintWriter w = response.getWriter(); + + w.write("\n" + + "\n" + + "\n" + + " \n" + + " Title\n" + + "\n" + + "\n" + + "
\n" + + " username: \n" + + " age: \n" + + " \n" + + "
\n" + + "\n" + + "\n"); } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberListServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberListServlet.java new file mode 100644 index 00000000..5a3dd1e6 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberListServlet.java @@ -0,0 +1,62 @@ +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members") +public class MemberListServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + List members = memberRepository.findAll(); + + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + + PrintWriter w = response.getWriter(); + w.write(""); + w.write(""); + w.write(" "); + w.write(" Title"); + w.write(""); + w.write(""); + w.write("메인"); + w.write(""); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); +/* + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); +*/ + for (Member member : members) { + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + } + w.write(" "); + w.write("
idusernameage
1userA10
" + member.getId() + "" + member.getUsername() + "" + member.getAge() + "
"); + w.write(""); + w.write(""); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberSaveServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberSaveServlet.java new file mode 100644 index 00000000..1c6159b7 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberSaveServlet.java @@ -0,0 +1,47 @@ +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save") +public class MemberSaveServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("MemberSaveServlet.service"); + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + PrintWriter w = response.getWriter(); + + w.write("\n" + + "\n" + + " \n" + + "\n" + + "\n" + + "성공\n" + + "
    \n" + + "
  • id="+member.getId()+"
  • \n" + + "
  • username="+member.getUsername()+"
  • \n" + + "
  • age="+member.getAge()+"
  • \n" + + "
\n" + + "메인\n" + + "\n" + + ""); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberFormServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberFormServlet.java new file mode 100644 index 00000000..01ead1c9 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberFormServlet.java @@ -0,0 +1,21 @@ +package hello.servlet.web.servletmvc; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form") +public class MvcMemberFormServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String viewPath = "/WEB-INF/views/new-form.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberListServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberListServlet.java new file mode 100644 index 00000000..56ef49e9 --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberListServlet.java @@ -0,0 +1,30 @@ +package hello.servlet.web.servletmvc; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members") +public class MvcMemberListServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + + request.setAttribute("members", members); + + String viewPath = "/WEB-INF/views/members.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} diff --git a/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberSaveServlet.java b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberSaveServlet.java new file mode 100644 index 00000000..31f9428d --- /dev/null +++ b/8 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberSaveServlet.java @@ -0,0 +1,34 @@ +package hello.servlet.web.servletmvc; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save") +public class MvcMemberSaveServlet extends HttpServlet { + + MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + + //Model에 데이터 보관. + request.setAttribute("member", member); + + String viewPath = "/WEB-INF/views/save-result.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} diff --git a/8 WEEK/servlet/src/main/resources/application.properties b/8 WEEK/servlet/src/main/resources/application.properties new file mode 100644 index 00000000..efc5ff2f --- /dev/null +++ b/8 WEEK/servlet/src/main/resources/application.properties @@ -0,0 +1 @@ +logging.level.org.apache.coyote.http11=debug diff --git a/8 WEEK/servlet/src/main/webapp/WEB-INF/views/members.jsp b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/members.jsp new file mode 100644 index 00000000..d9faff4a --- /dev/null +++ b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/members.jsp @@ -0,0 +1,27 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> + + + + Title + + +메인 + + + + + + + + + + + + + + + +
idusernameage
${item.id}${item.username}${item.age}
+ + \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/webapp/WEB-INF/views/new-form.jsp b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/new-form.jsp new file mode 100644 index 00000000..39d9e9b7 --- /dev/null +++ b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/new-form.jsp @@ -0,0 +1,15 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + Title + + + +
+ username: + age: + +
+ + \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/webapp/WEB-INF/views/save-result.jsp b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/save-result.jsp new file mode 100644 index 00000000..d3c0ed84 --- /dev/null +++ b/8 WEEK/servlet/src/main/webapp/WEB-INF/views/save-result.jsp @@ -0,0 +1,15 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + + +성공 +
    +
  • id=${member.id}
  • +
  • username=${member.username}
  • +
  • age=${member.age}
  • +
+메인 + + \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/webapp/basic.html b/8 WEEK/servlet/src/main/webapp/basic.html new file mode 100644 index 00000000..813a0b8e --- /dev/null +++ b/8 WEEK/servlet/src/main/webapp/basic.html @@ -0,0 +1,40 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/webapp/basic/hello-form.html b/8 WEEK/servlet/src/main/webapp/basic/hello-form.html new file mode 100644 index 00000000..a712358f --- /dev/null +++ b/8 WEEK/servlet/src/main/webapp/basic/hello-form.html @@ -0,0 +1,14 @@ + + + + + Title + + +
+ username: + age: + +
+ + \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/webapp/index.html b/8 WEEK/servlet/src/main/webapp/index.html new file mode 100644 index 00000000..22f9745d --- /dev/null +++ b/8 WEEK/servlet/src/main/webapp/index.html @@ -0,0 +1,86 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/webapp/jsp/members.jsp b/8 WEEK/servlet/src/main/webapp/jsp/members.jsp new file mode 100644 index 00000000..3a036b59 --- /dev/null +++ b/8 WEEK/servlet/src/main/webapp/jsp/members.jsp @@ -0,0 +1,36 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="hello.servlet.domain.member.Member" %> +<%@ page import="java.util.List" %> +<%@ page import="hello.servlet.domain.member.MemberRepository" %> +<% + MemberRepository memberRepository = MemberRepository.getInstance(); + + List members = memberRepository.findAll(); +%> + + + + Title + + +메인 + + + + + + + + <% + for (Member member : members) { + out.write(" "); + out.write(" "); + out.write(" "); + out.write(" "); + out.write(" "); + } + %> + +
idusernameage
" + member.getId() + "" + member.getUsername() + "" + member.getAge() + "
+ + \ No newline at end of file diff --git a/8 WEEK/servlet/src/main/webapp/jsp/members/new-form.jsp b/8 WEEK/servlet/src/main/webapp/jsp/members/new-form.jsp new file mode 100644 index 00000000..705255aa --- /dev/null +++ b/8 WEEK/servlet/src/main/webapp/jsp/members/new-form.jsp @@ -0,0 +1,13 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Title + + +
+ username: + age: + +
+ + diff --git a/8 WEEK/servlet/src/main/webapp/jsp/members/save.jsp b/8 WEEK/servlet/src/main/webapp/jsp/members/save.jsp new file mode 100644 index 00000000..6a102d8d --- /dev/null +++ b/8 WEEK/servlet/src/main/webapp/jsp/members/save.jsp @@ -0,0 +1,27 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="hello.servlet.domain.member.Member" %> +<%@ page import="hello.servlet.domain.member.MemberRepository" %> +<% + MemberRepository memberRepository = MemberRepository.getInstance(); + //request, response는 지원함 + System.out.println("MemberSaveServlet.service"); + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); +%> + + + Title + + + 성공 +
    +
  • id=<%=member.getId()%>
  • +
  • username=<%=member.getUsername()%>
  • +
  • age=<%=member.getAge()%>
  • +
+ 메인 + + diff --git a/8 WEEK/servlet/src/test/java/hello/servlet/ServletApplicationTests.java b/8 WEEK/servlet/src/test/java/hello/servlet/ServletApplicationTests.java new file mode 100644 index 00000000..9df6b76e --- /dev/null +++ b/8 WEEK/servlet/src/test/java/hello/servlet/ServletApplicationTests.java @@ -0,0 +1,13 @@ +package hello.servlet; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ServletApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/8 WEEK/servlet/src/test/java/hello/servlet/domain/member/MemberRepositoryTest.java b/8 WEEK/servlet/src/test/java/hello/servlet/domain/member/MemberRepositoryTest.java new file mode 100644 index 00000000..f6c6f6b3 --- /dev/null +++ b/8 WEEK/servlet/src/test/java/hello/servlet/domain/member/MemberRepositoryTest.java @@ -0,0 +1,46 @@ +package hello.servlet.domain.member; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MemberRepositoryTest { + + MemberRepository memberRepository = MemberRepository.getInstance(); + + @AfterEach + void afterEach() { + memberRepository.clearStore(); + } + + @Test + void save() { + Member member = new Member("Hello", 20); + + Member savedMember = memberRepository.save(member); + + Member findMember = memberRepository.findById(savedMember.getId()); + Assertions.assertThat(findMember).isEqualTo(savedMember); + + } + + @Test + void findAll() { + Member member1 = new Member("Hello1", 20); + Member member2 = new Member("Hello2", 20); + + memberRepository.save(member1); + memberRepository.save(member2); + + List memberList = memberRepository.findAll(); + + Assertions.assertThat(memberList.size()).isEqualTo(2); + Assertions.assertThat(memberList).contains(member1, member2); + } + + +} \ No newline at end of file diff --git a/9 WEEK/item-service/.gitignore b/9 WEEK/item-service/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/9 WEEK/item-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/9 WEEK/item-service/SECTION7.md b/9 WEEK/item-service/SECTION7.md new file mode 100644 index 00000000..17275fb6 --- /dev/null +++ b/9 WEEK/item-service/SECTION7.md @@ -0,0 +1,767 @@ +# 7. 스프링 MVC - 웹 페이지 만들기 + +**서비스 제공 흐름** +![S7-1.png](img%2FS7-1.png) + + +--- + + +## 상품 도메인 개발 + +*Item - 상품 객체* +```java +package hello.itemservice.domain.item; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Item { + private long id; + private String itemName; + private Integer price; //null 가능 + private Integer quantity; //null 가능 + + public Item(String itemName, Integer price, Integer quantity) { + this.itemName = itemName; + this.price = price; + this.quantity = quantity; + } +} +``` + +*ItemRepository - 상품 저장소* +```java +package hello.itemservice.domain.item; + +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Repository +public class ItemRepository { + + private static final Map store = new HashMap<>(); + private static long sequence = 0L; + + public Item save(Item item) { + item.setId(++sequence); + store.put(item.getId(), item); + return item; + } + + public Item findById(Long id) { + return store.get(id); + } + + public List findAll() { + return new ArrayList<>(store.values()); + } + + public void update(Long itemId, Item updateParam) { + Item findItem = findById(itemId); + findItem.setItemName(updateParam.getItemName()); + findItem.setPrice(updateParam.getPrice()); + findItem.setQuantity(updateParam.getQuantity()); + } + + public void clearStore() { + store.clear(); + } +} +``` + +*ItemRepositoryTest - 상품 저장소 테스트* +```java +package hello.itemservice.domain.item; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +class ItemRepositoryTest { + + ItemRepository itemRepository = new ItemRepository(); + + @AfterEach + void afterEach() { + itemRepository.clearStore(); + } + + @Test + void save() { + //give + Item item = new Item("itemA", 10000, 10); + //when + Item savedItem = itemRepository.save(item); + + //then + Item findItem = itemRepository.findById(item.getId()); + assertThat(findItem).isEqualTo(savedItem); + } + + @Test + void findAll() { + //give + Item item1 = new Item("item1", 10000, 10); + Item item2 = new Item("item2", 20000, 20); + + itemRepository.save(item1); + itemRepository.save(item2); + //when + List result = itemRepository.findAll(); + + //then + assertThat(result.size()).isEqualTo(2); + assertThat(result).contains(item1, item2); + } + + @Test + void update() { + //give + Item item1 = new Item("item1", 10000, 10); + Item savedItem = itemRepository.save(item1); + Long itemId = item1.getId(); + + //when + Item updateParam = new Item("item2", 20000, 20); + itemRepository.update(itemId, updateParam); + + //then + Item findItem = itemRepository.findById(itemId); + + assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName()); + assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice()); + assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity()); + } +} +``` + +--- + + +## 상품 서비스 HTML +- 상품 목록 HTML +- 상품 상세 HTML +- 상품 등록 폼 HTML +- 상품 수정 폼 HTML + + +--- + +## 상품 목록 - Thymeleaf + +*BasicItemController* +```java +package hello.itemservice.web.basic; + +import hello.itemservice.domain.item.Item; +import hello.itemservice.domain.item.ItemRepository; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.util.List; + +@Controller +@RequestMapping("/basic/items") +@RequiredArgsConstructor +public class BasicItemController { + + private final ItemRepository itemRepository; + + @GetMapping() + public String items(Model model) { + List itmes = itemRepository.findAll(); + model.addAttribute("items",itmes); + return "basic/items"; + } + + + @PostConstruct + public void init() { + itemRepository.save(new Item("itemA", 10000, 10)); + itemRepository.save(new Item("itemB", 20000, 20)); + } + +} +``` + +- `@RequiredArgsController` + - `final`이 붙은 멤버변수만 사용해 생성자들 자동으로 만듦 + + +*items.html* +```html + + + + + + + + +
+
+

상품 목록

+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + +
ID상품명가격수량
회원id상품명
+
+ +
+ + + +``` + +### 타임리프 간단히 알아보기 +**타임리프 사용 선언**
+`` + +**속성 변경 - th:href**
+`th:href="@{css/bootstrap.min.css"` +- `href="xxx1"`을 `th:href="xxx2"`의 값으로 변경 +- HTML을 그대로 볼 때는 `href`속성이 사용, 뷰 템플릿을 거치면 `th:href`의 값이 `href`로 대체돼 동적으로 변경 가능. +- 대부분의 HTML속성을 `th:xxx`로 변경 가능 + +**타임리프 핵심** +- 핵심 : `th:xxx`가 붙은 부분은 서버사이드에서 렌더링, 기존 것을 대체함. +`th:xxx`가 없으면 기존 html의 `xxx`속성이 그대로 사용됨. +- HTML을 파일로 직접 열었을 때, `th:xxx`가 있어도 웹 브라우저는 `th:`속성을 알지 못해 무시함. +- 따라서 HTML을 파일 보기를 유지하면서 템플릿 기능도 사용 가능. + +**상품 등록 폼으로 이동**
+**속성 변경 - `th:onclick`** +- `onclick="location.href='addForm.html'"` +- `th:onclick="|location.href='@{/basic/items/add}'|"` + +**리터럴 대체 - |...|**
+- 타임리프에서 문자와 표현식 등은 분리돼 있어 더해서 사용해야함. + - ex) `` +- 다음과 같이 리터럴 대체 문자를 사용하면, 더하기 없이 편리하게 사용 가능. + - ex) `` + +- 결과를 다음과 같이 만들어야 하는데 + - `location.href='basic/items/add'` +- 그냥 사용하면 문자와 표현식을 따로 더해서 사용해야 하므로 복잡해짐. + - `th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"` +- 리터럴 대체 문법을 사용하면 다음과 같이 편리하게 사용 가능 + - `th:onclick="|location.href='@{/basic/items/add}'|"` + +**반복 출력 - th:each** +- `10000` +- 내용의 값을 th:text 의 값으로 변경. +- 여기서는 10000을 ${item.price} 의 값으로 변경. + +**URL 링크 표현식2 - @{...}** +- `th:href="@{/basic/items/{itemId}(itemId=${item.id})}"` +- 경로 변수 `({itemId})`뿐만 아니라 쿼리 파라미터도 생성. +- ex) `th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"` + - 생성 링크: http://localhost:8080/basic/items/1?query=test + +**URL 링크 간단히** +- th:href="@{|/basic/items/${item.id}|}" +- 리터럴 대체 문법을 활용해서 간단히 사용할 수 있음. + + +> 타임리프는 순수 HTML 파일을 웹 브라우저에서 열어도 내용 확인이 가능, 서버를 통해 뷰 템플릿을 거치면 +> 동적으로 변경된 결과를 확인할 수 있음. 하지만 JSP 파일은 웹 브러우저에서 그냥 열면 JSP 코드와 +> HTML 이 섞여 있어 정상적인 확인이 불가능. 오직 서버를 통해 JSP 를 열어야 함.
+> 이렇게 순수 **HTML 을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿**이라고 함. + + +--- + + +## 상품 상세 + +*item method* +```java + @GetMapping("/{itemId}") + public String item(@PathVariable("itemId") Long itemId, Model model) { + Item item = itemRepository.findById(itemId); + model.addAttribute("item", item); + return "basic/item"; + } +``` + +`PathVariable`로 넘어온 상품ID로 상품을 조회하고, 모델에 담아둠. 그리고 뷰 템플릿을 호출. + + +*item.html* +```html + + + + + + + + + +
+ +
+

상품 상세

+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ + +``` + +**속성 변경 - th:value**
+`th:value="${item.id}"` +- 모델이 있는 item 정보를 획득하고 프로퍼티 접근법으로 출력한다. `(item.getId())` +- `value` 속성을 `th:value` 속성으로 변경. + +**상품 수정 링크** +- `th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"` + +**목록으로 링크** +- `th:onclick="|location.href='@{/basic/items}'|"` + + +## 상품 등록 폼 + +*addForm method* +```java + @GetMapping("/add") + public String addForm() { + return "basic/addForm"; + } +``` + +*addForm.html - 상품 등록 폼 뷰* +```html + + + + + + + + + +
+ +
+

상품 등록 폼

+
+ +

상품 입력

+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + +``` + +**속성 변경 - th:action** +- `th:action` +- HTML form에서 `action` 에 값이 없으면 현재 URL에 데이터를 전송한다. +- 상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메서드로 두 기능을 구분한다. + - 상품 등록 폼: GET `/basic/items/add` + - 상품 등록 처리: POST `/basic/items/add` +-이렇게 하면 하나의 URL로 등록 폼과, 등록 처리를 깔끔하게 처리할 수 있다. + +**취소** +- 취소시 상품 목록으로 이동한다. +- `th:onclick="|location.href='@{/basic/items}'|"` + + +## 상품 등록 처리 - @ModelAttribute + +상품 등록 폼은 다음 방식으로 서버에 데이터를 전달. +- POST - HTML Form + - `content-type: application/x-www-form-urlencoded` + - 메시지 바디에 쿼리 파리미터 형식으로 전달 `itemName=itemA&price=10000&quantity=10` + - 예) 회원 가입, 상품 주문, HTML Form 사용 + +요청 파라미터 형식을 처리하기 위해 `@RequestParam`을 사용해야 함. + + + +### 상품 등록 처리 - @RequestParam + +*addItemV1 method* +```java +@PostMapping("/add") +public String addItemV1(@RequestParam String itemName, + @RequestParam int price, + @RequestParam Integer quantity, + Model model) { + Item item = new Item(); + item.setItemName(itemName); + item.setPrice(price); + item.setQuantity(quantity); + itemRepository.save(item); + model.addAttribute("item", item); + return "basic/item"; +} +``` +- @RequestParam String itemName` : itemName 요청 파라미터 데이터를 해당 변수에 받음. +- `Item` 객체를 생성하고 `itemRepository` 를 통해서 저장. +- 저장된 item 을 모델에 담아서 뷰에 전달. + + +### 상품 등록 처리 - @ModelAttribute + +*addItemV2 method* +```java +@PostMapping("/add") +public String addItemV2(@ModelAttribute("item") Item item, Model model) { + itemRepository.save(item); + //model.addAttribute("item", item); //자동 추가, 생략 가능 + return "basic/item"; +} +``` + +**@ModelAttribute - 요청 파라미터 처리** +- `@ModelAttribute` 는 Item 객체를 생성, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력. + +**@ModelAttribute - Model 추가** +- `@ModelAttribute` 는 중요한 한가지 기능이 더 있는데, 바로 모델(Model)에 `@ModelAttribute`로 지정한 객체를 자동으로 넣어줌. +코드를 보면 model.addAttribute("item", item) 가 주석처리 되어 있어도 잘 동작하는 것을 확인 가능 + +모델에 데이터를 담을 때는 이름이 필요. 이름은 `@ModelAttribute`에 지정한 `name(value)`속성을 사용한다. +만약 다음과 같이 `@ModelAttribute`의 이름을 다르게 지정하면 다른 이름으로 모델에 포함. + +EX)
+- @ModelAttribute("hello") Item item 이름을 hello 로 지정 +- model.addAttribute("hello", item); 모델에 hello 이름으로 저장 + + +*addItemV3 - 상품 등록 처리 - Model Attribute 이름 생략* +```java +@PostMapping("/add") +public String addItemV3(@ModelAttribute Item item) { + itemRepository.save(item); + return "basic/item"; +} +``` +`@ModelAttribute`의 이름 생략 가능 + +**주의**
+`@ModelAttribute` 의 이름을 생략하면 모델에 저장될 때 클래스명을 사용. +이때 클래스의 첫글자만 소문자로 변경해서 등록. + +EX)
+- `@ModelAttribute`클래스명 -> 모델에 자동 추가되는 이름 + - `Item -> item` + - `HelloWorld -> helloWorld` + + +*addItemV4 - 상품 등록 처리 - ModelAttribute 전체 생략* +```java +@PostMapping("/add") +public String addItemV4(Item item) { + itemRepository.save(item); + return "basic/item"; +} +``` +`@ModelAttribute`자체도 생략 가능. 대상 객체는 모델에 자동 등록. 나머지 사항은 기존과 동일. + + + +## 상품 수정 + +*editForm method - 상품 수정 폼 컨트롤러* +```java + @GetMapping("/{itemId}/edit") + public String editForm(@PathVariable("itemId") Long itemId, Model model) { + Item item = itemRepository.findById(itemId); + model.addAttribute("item", item); + return "basic/editForm"; + } +``` + + +*editForm.html - 상품 수정 폼 뷰* +```html + + + + + + + + + +
+ +
+

상품 수정 폼

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + +``` + + +### 상품 수정 개발 +```java + @PostMapping("/{itemId}/edit") + public String edit(@PathVariable("itemId") Long itemId, @ModelAttribute Item item) { + itemRepository.update(itemId, item); + + return "redirect:/basic/items/{itemId}"; + } +``` + +- GET `/items/{itemId}/edit` : 상품 수정 폼 +- POST `/items/{itemId}/edit` : 상품 수정 처리 + + +**리다이렉트**
+상품 수정은 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동하도록 리다이렉트 호출 +- 스프링은 `redirect:/....`으로 편리하게 리다이레그를 지원. +- `redirect:/basic/items/{itemId}` + - 컨트롤러에 매핑된 `@PathVariable`의 값은 `redirect` 에도 사용 가능. + - `redirect:/basic/items/{itemId}` -> `{itemId}`는 + `@PathVariable("itemId") Long itemId` 의 값을 그대로 사용함. + + + +## PRG Post/Redirect/Get +현재 코드에는 문제가 있다. 상품 등록 후 새로고침을 할 때 상품이 중복해서 등록된다. + +**POST 등록 후 새로고침** +![S7-2.png](img%2FS7-2.png) + +상품 등록 폼에서 데이터 입력, 저장을 선택하면 `POST /add` + 상품 데이터로 서버로 전송
+이 상태에서 새로고침을 하면 마지막에 전송한 `POST /add` + 상품 데이터로 다시 전송하게 된다.
+그래서 내용은 같고, ID만 다른 상품 데이터가 쌓인다. + + + +**POST, Redirect, GET** +![S7-3.png](img%2FS7-3.png) + +새로 고침 문제를 해결하려면 상품 저장 후 뷰 템플릿으로 이동하는 것이 아니라, 상품 상세 화면으로 리다이렉트 호출하면 된다.
+웹 브라우저는 리다이렉트의 영향으로 사움 저장 후 실제 상품 상세 화면으로 다시 이동한다. +따라서 마지막에 호출한 내용이 상품 상세 화면인 `GET /items/{id}`가 된다. + +*save method 수정* +```java + @PostMapping("/add") + public String save(@ModelAttribute("item") Item item, Model model) { + itemRepository.save(item); + //model.addAttribute("item", item); //자동 추가, 생략 가능 +// return "basic/item"; + return "redirect:/basic/items/" + item.getId(); + } +``` + +이런 문제 해결 방식을 `PRG Post/Redirect/Get`라 한다. + + +## RedirectAttributes +상품이 저장이 잘 됐다면 "저장 완료"라는 메시지를 보여주게 수정한다. + +*save method 수정* +```java + @PostMapping("/add") + public String save(@ModelAttribute("item") Item item, Model model, RedirectAttributes redirectAttributes) { + Item savedItem = itemRepository.save(item); +// model.addAttribute("item", item); //자동 추가, 생략 가능 +// return "basic/item"; + redirectAttributes.addAttribute("savedItem", savedItem.getId()); + redirectAttributes.addAttribute("status", true); + return "redirect:/basic/items/{savedItem}"; + } +``` +실행 시 리다이렉트 결과
+`http://localhost:8080/basic/items/3?status=true` + +**RedirectAttributes**
+`RedirectAttributes`를 사용하면 URL인코딩도 해주고, `pathVariable`, 쿼리 파라미터도 처리함. +- `redirect:/basic/items/{itemId}` +- pathVariable 바인딩: `{itemId}` +- 나머지는 쿼리 파라미터로 처리: `?status=true` + + +*item.html - 뷰 템플릿 메시지 추가* +```html +
+

상품 상세

+
+ +

+ +
+``` + +- `th:if` : 해당 조건이 참이면 실행 +- `${param.status}` : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능 + - 원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 함. + 쿼리 파라미터는 자주 사용해서 타임리프에서 직접 지원. + + + + + +--- \ No newline at end of file diff --git a/9 WEEK/item-service/build.gradle b/9 WEEK/item-service/build.gradle new file mode 100644 index 00000000..a65c94f1 --- /dev/null +++ b/9 WEEK/item-service/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.1' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'hello' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.jar b/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.jar differ diff --git a/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.properties b/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1af9e093 --- /dev/null +++ b/9 WEEK/item-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/9 WEEK/item-service/gradlew b/9 WEEK/item-service/gradlew new file mode 100644 index 00000000..1aa94a42 --- /dev/null +++ b/9 WEEK/item-service/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/9 WEEK/item-service/gradlew.bat b/9 WEEK/item-service/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/9 WEEK/item-service/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/9 WEEK/item-service/img/S7-1.png b/9 WEEK/item-service/img/S7-1.png new file mode 100644 index 00000000..5df8b1ce Binary files /dev/null and b/9 WEEK/item-service/img/S7-1.png differ diff --git a/9 WEEK/item-service/img/S7-2.png b/9 WEEK/item-service/img/S7-2.png new file mode 100644 index 00000000..ab27260d Binary files /dev/null and b/9 WEEK/item-service/img/S7-2.png differ diff --git a/9 WEEK/item-service/img/S7-3.png b/9 WEEK/item-service/img/S7-3.png new file mode 100644 index 00000000..e301cd83 Binary files /dev/null and b/9 WEEK/item-service/img/S7-3.png differ diff --git a/9 WEEK/item-service/settings.gradle b/9 WEEK/item-service/settings.gradle new file mode 100644 index 00000000..df5bd80b --- /dev/null +++ b/9 WEEK/item-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'item-service' diff --git a/9 WEEK/item-service/src/main/java/hello/itemservice/ItemServiceApplication.java b/9 WEEK/item-service/src/main/java/hello/itemservice/ItemServiceApplication.java new file mode 100644 index 00000000..1311934b --- /dev/null +++ b/9 WEEK/item-service/src/main/java/hello/itemservice/ItemServiceApplication.java @@ -0,0 +1,13 @@ +package hello.itemservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ItemServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(ItemServiceApplication.class, args); + } + +} diff --git a/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/Item.java b/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/Item.java new file mode 100644 index 00000000..ada50303 --- /dev/null +++ b/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/Item.java @@ -0,0 +1,21 @@ +package hello.itemservice.domain.item; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class Item { + private long id; + private String itemName; + private Integer price; //null 가능 + private Integer quantity; //null 가능 + + public Item(String itemName, Integer price, Integer quantity) { + this.itemName = itemName; + this.price = price; + this.quantity = quantity; + } +} diff --git a/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/ItemRepository.java b/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/ItemRepository.java new file mode 100644 index 00000000..c4f3fbc8 --- /dev/null +++ b/9 WEEK/item-service/src/main/java/hello/itemservice/domain/item/ItemRepository.java @@ -0,0 +1,41 @@ +package hello.itemservice.domain.item; + +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Repository +public class ItemRepository { + + private static final Map store = new HashMap<>(); + private static long sequence = 0L; + + public Item save(Item item) { + item.setId(++sequence); + store.put(item.getId(), item); + return item; + } + + public Item findById(Long id) { + return store.get(id); + } + + public List findAll() { + return new ArrayList<>(store.values()); + } + + public void update(Long itemId, Item updateParam) { + Item findItem = findById(itemId); + findItem.setItemName(updateParam.getItemName()); + findItem.setPrice(updateParam.getPrice()); + findItem.setQuantity(updateParam.getQuantity()); + } + + public void clearStore() { + store.clear(); + } +} + diff --git a/9 WEEK/item-service/src/main/java/hello/itemservice/web/basic/BasicItemController.java b/9 WEEK/item-service/src/main/java/hello/itemservice/web/basic/BasicItemController.java new file mode 100644 index 00000000..8c79df4e --- /dev/null +++ b/9 WEEK/item-service/src/main/java/hello/itemservice/web/basic/BasicItemController.java @@ -0,0 +1,94 @@ +package hello.itemservice.web.basic; + +import hello.itemservice.domain.item.Item; +import hello.itemservice.domain.item.ItemRepository; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import java.util.List; + +@Controller +@RequestMapping("/basic/items") +@RequiredArgsConstructor +public class BasicItemController { + + private final ItemRepository itemRepository; + + @GetMapping() + public String items(Model model) { + List itmes = itemRepository.findAll(); + model.addAttribute("items",itmes); + return "basic/items"; + } + + + @GetMapping("/{itemId}") + public String item(@PathVariable("itemId") Long itemId, Model model) { + Item item = itemRepository.findById(itemId); + model.addAttribute("item", item); + return "basic/item"; + } + + + @GetMapping("/add") + public String addForm() { + return "basic/addForm"; + } + + +// @PostMapping("/add") +// public String save( +// @RequestParam String itemName, +// @RequestParam int price, +// @RequestParam Integer quantity, +// Model model +// ) { +// Item item = new Item(itemName, price, quantity); +// itemRepository.save(item); +// model.addAttribute("item", item); +// return "basic/item"; +// } + + @PostMapping("/add") + public String save(@ModelAttribute("item") Item item, Model model, RedirectAttributes redirectAttributes) { + Item savedItem = itemRepository.save(item); +// model.addAttribute("item", item); //자동 추가, 생략 가능 +// return "basic/item"; + redirectAttributes.addAttribute("savedItem", savedItem.getId()); + redirectAttributes.addAttribute("status", true); + return "redirect:/basic/items/{savedItem}"; + } + + + + + @GetMapping("/{itemId}/edit") + public String editForm(@PathVariable("itemId") Long itemId, Model model) { + Item item = itemRepository.findById(itemId); + model.addAttribute("item", item); + return "basic/editForm"; + } + + @PostMapping("/{itemId}/edit") + public String edit(@PathVariable("itemId") Long itemId, @ModelAttribute Item item) { + itemRepository.update(itemId, item); + + return "redirect:/basic/items/{itemId}"; + } + + + + + @PostConstruct + public void init() { + itemRepository.save(new Item("itemA", 10000, 10)); + itemRepository.save(new Item("itemB", 20000, 20)); + } + +} + + diff --git a/9 WEEK/item-service/src/main/resources/application.properties b/9 WEEK/item-service/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/9 WEEK/item-service/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/9 WEEK/item-service/src/main/resources/static/css/bootstrap.min.css b/9 WEEK/item-service/src/main/resources/static/css/bootstrap.min.css new file mode 100644 index 00000000..edfbbb03 --- /dev/null +++ b/9 WEEK/item-service/src/main/resources/static/css/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.0.2 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0))}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + (.5rem + 2px));padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + (1rem + 2px));padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + (.75rem + 2px))}textarea.form-control-sm{min-height:calc(1.5em + (.5rem + 2px))}textarea.form-control-lg{min-height:calc(1.5em + (1rem + 2px))}.form-control-color{max-width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast:not(.showing):not(.show){opacity:0}.toast.hide{display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1060;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1050;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{color:#0d6efd!important}.text-secondary{color:#6c757d!important}.text-success{color:#198754!important}.text-info{color:#0dcaf0!important}.text-warning{color:#ffc107!important}.text-danger{color:#dc3545!important}.text-light{color:#f8f9fa!important}.text-dark{color:#212529!important}.text-white{color:#fff!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-reset{color:inherit!important}.bg-primary{background-color:#0d6efd!important}.bg-secondary{background-color:#6c757d!important}.bg-success{background-color:#198754!important}.bg-info{background-color:#0dcaf0!important}.bg-warning{background-color:#ffc107!important}.bg-danger{background-color:#dc3545!important}.bg-light{background-color:#f8f9fa!important}.bg-dark{background-color:#212529!important}.bg-body{background-color:#fff!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/9 WEEK/item-service/src/main/resources/static/html/addForm.html b/9 WEEK/item-service/src/main/resources/static/html/addForm.html new file mode 100644 index 00000000..22089e3b --- /dev/null +++ b/9 WEEK/item-service/src/main/resources/static/html/addForm.html @@ -0,0 +1,51 @@ + + + + + + + + + +
+ +
+

상품 등록 폼

+
+ +

상품 입력

+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + \ No newline at end of file diff --git a/9 WEEK/item-service/src/main/resources/static/html/editForm.html b/9 WEEK/item-service/src/main/resources/static/html/editForm.html new file mode 100644 index 00000000..32d21dfc --- /dev/null +++ b/9 WEEK/item-service/src/main/resources/static/html/editForm.html @@ -0,0 +1,53 @@ + + + + + + + + + +
+ +
+

상품 수정 폼

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + \ No newline at end of file diff --git a/9 WEEK/item-service/src/main/resources/static/html/item.html b/9 WEEK/item-service/src/main/resources/static/html/item.html new file mode 100644 index 00000000..51b57f3b --- /dev/null +++ b/9 WEEK/item-service/src/main/resources/static/html/item.html @@ -0,0 +1,50 @@ + + + + + + + + + +
+ +
+

상품 상세

+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ + \ No newline at end of file diff --git a/9 WEEK/item-service/src/main/resources/static/html/items.html b/9 WEEK/item-service/src/main/resources/static/html/items.html new file mode 100644 index 00000000..73732eb4 --- /dev/null +++ b/9 WEEK/item-service/src/main/resources/static/html/items.html @@ -0,0 +1,52 @@ + + + + + + + + +
+
+

상품 목록

+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
ID상품명가격수량
1테스트 상품11000010
2테스트 상품22000020
+
+ +
+ + + \ No newline at end of file diff --git a/9 WEEK/item-service/src/main/resources/static/index/index.html b/9 WEEK/item-service/src/main/resources/static/index/index.html new file mode 100644 index 00000000..4daf829d --- /dev/null +++ b/9 WEEK/item-service/src/main/resources/static/index/index.html @@ -0,0 +1,16 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/9 WEEK/item-service/src/main/resources/templates/basic/addForm.html b/9 WEEK/item-service/src/main/resources/templates/basic/addForm.html new file mode 100644 index 00000000..8d85fec2 --- /dev/null +++ b/9 WEEK/item-service/src/main/resources/templates/basic/addForm.html @@ -0,0 +1,56 @@ + + + + + + + + + +
+ +
+

상품 등록 폼

+
+ +

상품 입력

+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + \ No newline at end of file diff --git a/9 WEEK/item-service/src/main/resources/templates/basic/editForm.html b/9 WEEK/item-service/src/main/resources/templates/basic/editForm.html new file mode 100644 index 00000000..b35b4482 --- /dev/null +++ b/9 WEEK/item-service/src/main/resources/templates/basic/editForm.html @@ -0,0 +1,58 @@ + + + + + + + + + +
+ +
+

상품 수정 폼

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + \ No newline at end of file diff --git a/9 WEEK/item-service/src/main/resources/templates/basic/item.html b/9 WEEK/item-service/src/main/resources/templates/basic/item.html new file mode 100644 index 00000000..acb5b463 --- /dev/null +++ b/9 WEEK/item-service/src/main/resources/templates/basic/item.html @@ -0,0 +1,59 @@ + + + + + + + + + +
+ +
+

상품 상세

+
+ +

+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ + \ No newline at end of file diff --git a/9 WEEK/item-service/src/main/resources/templates/basic/items.html b/9 WEEK/item-service/src/main/resources/templates/basic/items.html new file mode 100644 index 00000000..186ccae8 --- /dev/null +++ b/9 WEEK/item-service/src/main/resources/templates/basic/items.html @@ -0,0 +1,49 @@ + + + + + + + + +
+
+

상품 목록

+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + +
ID상품명가격수량
회원id상품명
+
+ +
+ + + \ No newline at end of file diff --git a/9 WEEK/item-service/src/test/java/hello/itemservice/ItemServiceApplicationTests.java b/9 WEEK/item-service/src/test/java/hello/itemservice/ItemServiceApplicationTests.java new file mode 100644 index 00000000..e2ded1be --- /dev/null +++ b/9 WEEK/item-service/src/test/java/hello/itemservice/ItemServiceApplicationTests.java @@ -0,0 +1,13 @@ +package hello.itemservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ItemServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/9 WEEK/item-service/src/test/java/hello/itemservice/domain/item/ItemRepositoryTest.java b/9 WEEK/item-service/src/test/java/hello/itemservice/domain/item/ItemRepositoryTest.java new file mode 100644 index 00000000..af05ff04 --- /dev/null +++ b/9 WEEK/item-service/src/test/java/hello/itemservice/domain/item/ItemRepositoryTest.java @@ -0,0 +1,65 @@ +package hello.itemservice.domain.item; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +class ItemRepositoryTest { + + ItemRepository itemRepository = new ItemRepository(); + + @AfterEach + void afterEach() { + itemRepository.clearStore(); + } + + @Test + void save() { + //give + Item item = new Item("itemA", 10000, 10); + //when + Item savedItem = itemRepository.save(item); + + //then + Item findItem = itemRepository.findById(item.getId()); + assertThat(findItem).isEqualTo(savedItem); + } + + @Test + void findAll() { + //give + Item item1 = new Item("item1", 10000, 10); + Item item2 = new Item("item2", 20000, 20); + + itemRepository.save(item1); + itemRepository.save(item2); + //when + List result = itemRepository.findAll(); + + //then + assertThat(result.size()).isEqualTo(2); + assertThat(result).contains(item1, item2); + } + + @Test + void update() { + //give + Item item1 = new Item("item1", 10000, 10); + Item savedItem = itemRepository.save(item1); + Long itemId = item1.getId(); + + //when + Item updateParam = new Item("item2", 20000, 20); + itemRepository.update(itemId, updateParam); + + //then + Item findItem = itemRepository.findById(itemId); + + assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName()); + assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice()); + assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity()); + } +} \ No newline at end of file diff --git a/9 WEEK/servlet/.gitignore b/9 WEEK/servlet/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/9 WEEK/servlet/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/9 WEEK/servlet/SECTION1.md b/9 WEEK/servlet/SECTION1.md new file mode 100644 index 00000000..4f8c2930 --- /dev/null +++ b/9 WEEK/servlet/SECTION1.md @@ -0,0 +1,201 @@ +# 스프링 웹 MVC + +## 웹 서버, 웹 애플리케이션 서버 + +### 모든 것이 HTTP +HTTP 메시지에 모든 것을 전송 +- HTML, TEXT +- IMAGE, 음성, 영상, 파일 +- JSON, XML(API) +- 거의 모든 형태의 데이터 전송 가능 +- 서버간에 데이터를 주고 받을 때도 대부분 HTTP 사용 +- **지금은 HTTP 시대** + +### 웹 서버(Web Server) +- HTTP 기반으로 동작 +- 정적 리소스 제공, 기타 부가기능 +- 정적(파일) HTML, CSS, JS, 이미지, 영상 +- 예) NGINX, APACHE + +### 웹 애플리케이션 서버(WAS - Web Application Server) +- HTTP 기반으로 동작 +- 웹 서버 기능 포함 + (정적 리소스 제공 가능) +- 프로그램 코드를 실행해서 애플리케이션 로직 수행 + - 동적 HTML, HTTP API(JSON) + - Servlet, Jsp, Spring MVC +- 예) Tomcat, Jetty, Undertow + +### 웹 서버, 웹 애플리케이션 서버(WAS) 차이 +- 웹 서버는 정적 리소스(파일), WAS는 애플리케이션 로직 +- 둘의 용어 경계가 모호함 + - WS 서버도 프로그램을 실행하는 기능을 포함하기도 함 + - WAS도 WS의 기능을 제공 +- 자바는 서블릿 컨테이너 기능을 제공하면 WAS + - 서블릿 없이 자바코드를 실행하는 서버 프레임워크도 존재 + + +### 웹 시스템 구성 - WAS, DB +- WAS, DB만으로 시스템 구성 가능 +- WAS는 정적 리소스, 애플리케이션 로직 모두 제공 가능 + + +- WAS가 너무 많은 역할을 담당 -> 서버 과부하 우려 +- 가장 비싼 애플리케이션 로직이 정적 리소스 때문에 수행이 어려울 수 있음 +- WAS 장애시 오류 화면도 노출 불가 + +### 웹 시스템 구성 - WEB, WAS, DB +- 정적 리소스는 웹 서버가 처리 +- 웹 서버는 애플리케이션 로직같은 동적인 처리가 필요하면 WAS에 요청을 위임 +- WAS는 중요한 애플리케이션 로직 처리 전담 + +>WS = HTML, CSS, JS, 이미지 처리
+>WAS = 애플리케이션 로직 처리 + +- 효율적인 리소스 관리 + - 정적 리소스가 많이 사용되면 WS 증설 + - 애플리케이션 리소스가 많이 사용되면 WAS 증설 +- 정적 리소스만 제공하는 WS는 잘 죽지 않음 +- 애플리캐이션 로직이 동작하는 WAS서버는 잘 죽음 +- WAS, DB 장애시 WEB 서버가 오류 화면 제공 가능 + + +## Servlet +### HTML Form 데이터 전송 +#### POST 전송 - 저장 +![S1-1.png](img%2FS1-1.png) + + +### 서버에서 처리해야 하는 업무 +#### 웹 애플리케이션 서버 직접 구현시 + - ![S1-2.png](img%2FS1-2.png) +#### 서블릿을 지원하는 WAS 사용시 + - ![S1-3.png](img%2FS1-3.png) + +### 서블릿 +#### 특징 +```java +@WebServlet(name = "helloServlet", urlPatterns = "/hello") +public class HelloServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response){ + //애플리케이션 로직 + } +``` +- urlPatterns(/hello)의 URL 호출되면 서블릿 코드 실행 +- HTTP 요청 정보를 편리하게 사용할 수 있는 HttpServletRequest +- HTTP 응답 정보를 편리하게 제공할 수 있는 HttpServletResponse +![S1-4.png](img%2FS1-4.png) + +#### HTTP 요청, 응답 흐름 +- HTTP 요청시 + - WAS는 Request, Response 객체를 새로 만들어 서블릿 객체 호출 + - 개발자는 Request객체에서 HTTP요청 정보를 편리하게 꺼내 사용 + - 개발자는 Response 객체에 HTTP 응답 정보를 편리하게 입력 + - WAS는 Response 객체에 담겨있는 내용으로 HTTP 응답 정보를 생성 + + +#### 서블릿 컨테이너 +![S1-5.png](img%2FS1-5.png) + +- 톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 함 +- 서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기 관리 +- 서블릿 객체는 싱글톤으로 관리 + - 고객의 요청이 올 때 마다 계속 객체를 생성하는 것은 비효율 + - 최초 로딩 시점에 서블릿 객체를 미리 만들어두고 재활용 + - 모든 고객 요청은 동일한 서블릿 객체 인스턴스에 접근 + - 공유 변수 사용 주의 + - 서블릿 컨테이너 종료시 함께 종료 +- JSP도 서블릿으로 변환 되어서 사용 +- 동시 요청을 위한 멀티 스레드 처리 지원 + + +## 동시 요청 - 멀티 스레드 + +### 스레드 +- 애플리케이션 코드를 하나하나 순차적으로 실행하는 것 = 스레드 +- 자바 메인 메서드 첫 실행 시 main이라는 이름의 스레드 실행 +- 스레드가 없다면 자바 애플리케이션 실행 불가 +- 스레드는 한번에 하나의 코드 라인 실행 +- 동시 처리가 필요하면 스레드를 추가로 생성 + +### 요청 마다 스레드 생성 + +- 장점 + - 동시 요청 처리 가능 + - 리소스(CPU, 메모리)가 허용할 때 까지 처리 가능 + - 하나의 스레드 지연 돼도, 나머지 스레드 정상 작동 +- 단점 + - 스레드는 생성 비용이 매우 비쌈 + - 고객의 요청이 올 때마다 스레드 생성 시, 응답 속도가 느림 + - 스레드는 컨텍스트 스위칭 비용 발생 + - 스레드 생성에 제한이 없음. + - 고객 요청이 너무 많이 오면, CPU, 메모리 임계점을 넘어 서버가 죽을 수 있다. + +### 스레드 풀 +#### 요청 마다 스레드 생성의 단점 보완 +- 특징 + - 필요한 스레드를 스레드 풀에 보관하고 관리. + - 스레드 풀에 생성 가능한 스레드의 최대치를 관리. 톰캣은 최대 200개 기본 설정 +- 사용 + - 스레드가 필요하면, 이미 생성되어 있는 스레드를 스레드 풀에서 꺼내 사용. + - 사용을 종료하면 스레드 풀에 해당 스레드 반납. + - 최대 스레드가 모두 사용중이어서 스레드 풀에 스레드가 없다면 + - 기다리는 요청은 거절하거나 특정 숫자만틈만 대기하도록 설정 가능 +- 장점 + - 스레드가 미리 생성돼 있어, 스레드를 생성하고 종료하는 비용(CPU)가 절약, 응답 시간이 빠름 + - 생성 가능한 스레드의 최대치가 있어 너무 많은 요청이 들어와도 기존 요청을 안전하게 처리 가능 + +### WAS의 멀티 스레드 지원 +- 멀티 스레드에 대한 부분은 WAS가 처리 +- 개발자가 멀티 스레드 관련 코드를 신경쓰지 않아도 됨 +- 개발자는 마치 싱글 스레드 프로그래밍 하듯 편리하게 코드 개발 가능 +- 멀티 스레드 환경이므로 싱글톤 객체(서블릿, 스프링 빈)는 주의해서 사용 + + + +## HTML, HTTP API, CSR, SSR + +### 정적 리소스 +- 고정된 HTML 파일, CSS, JS, 이미지, 영상 등을 제공 +- 주로 웹 브라우저 + +### HTML 페이지 +- 동적으로 필요한 HTML 파일을 생성해 전달 +- 웹 브라우저 : HTML 해석 + +### HTML API +- HTML이 아니라 데이터 전달 +- 주로 JSON 형식 사용 +- 다양한 시스템에서 호출 +- 데이터만 주고 받음, UI 화면이 필요하면, 클라이언트가 별도 처리 +- 앱, 웹 클라이언트, 서버 to 서버 +![S1-6.png](img%2FS1-6.png) + +#### 다양한 시스템 연동 +- 주로 JSON 형태로 데이터 통신 +- UI클라이언트 접점 + - 앱 클라이언트(아이폰, 안드로이드, PC앱) + - 웹 브라우저에서 JS를 통한 HTTP API 호출 + - React, Vue.js 같은 웹 클라이언트 +- 서버 to 서버 + - 주문 서버 -> 결제 서버 + - 기업간 데이터 통신 + +### 서버 사이드 렌더링, 클라이언트 사이드 렌더링 +#### - SSR - 서버 사이드 렌더링 + - HTML 최종 결과를 서버에서 만들어 웹 브라우저에 전달 + - 주로 정적인 화면에 사용 + - 관련기술 : JSP, 타임리프 -> BE 개발자 +![S1-7.png](img%2FS1-7.png) + +#### - CSR - 클라이언트 사이드 렌더링 + - HTML 결과를 자바스크립트를 사용해 웹 브라우저에서 동적으로 생성해 적용 + - 주로 동적인 화면에 사용, 웹 환경을 마치 앱 처럼 필요한 부분만 변경할 수 있음 + - 예) 구글지도, Gmail, 구글 캘린더 + - 관련기술 : React, Vue.js -> FE 개발자 +![S1-8.png](img%2FS1-8.png) + + +- 참고 + - React, Vue.js를 CSR + SSR 동시에 진원하는 웹 프레임워크 존재 + - SSR을 사용하더라도 자바스크립트를 사용해 화면 일부를 동적으로 변경 가능 diff --git a/9 WEEK/servlet/SECTION2.md b/9 WEEK/servlet/SECTION2.md new file mode 100644 index 00000000..17c3e7e9 --- /dev/null +++ b/9 WEEK/servlet/SECTION2.md @@ -0,0 +1,826 @@ +# 서블릿 + + +## Hello 서블릿 + +> **참고**
+> 서블릿은 톰캣 같은 WAS 서버를 직접 설치, 그 위에 서블리 ㅅ코드를 클래스 파일로 빌드해서 +> 올린 다음, 톰캣 서버를 실행한다. 이 과정은 매우 번거롭다.
+> 스프링 부트는 톰캣 서버를 내장하고 있으므로, 톰캣 서버 설치 없이 편리하게 서블릿 코드를 실행할 수 있다. + + +### 스프링 부트 서블릿 환경 구성 + +`@ServletComponentScan`
+스프링 부트는 서블릿을 직접 등록해서 사용할 수 있도록 `@ServletComponentScan`을 지원한다. +다음과 같이 추가한다. + +*ServletApplication* +```java +package hello.servlet; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.ServletComponentScan; + +@ServletComponentScan //서블릿 자동 등록 +@SpringBootApplication +public class ServletApplication { + + public static void main(String[] args) { + SpringApplication.run(ServletApplication.class, args); + } + +} +``` +
+ +**서블릿 등록하기** + +*HelloServlet* +```java +package hello.servlet.basic; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "helloServlet", urlPatterns = "/hello") +public class HelloServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException { + System.out.println("HelloServlet.service"); + System.out.println("requset = " + requset); + System.out.println("response = " + response); + + String username = requset.getParameter("username"); + System.out.println("username = " + username); + + response.setContentType("text/plain"); + response.setCharacterEncoding("utf-8"); + response.getWriter().write("hello " + username); + } +} +``` + +- `@WebServlet` 서블릿 애노테이션 + - name : 서블릿 이름 + - urlPatterns : URL매핑 + +HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 다음 메서드를 실행
+`protected void service(HttpServletRequest requset, HttpServletResponse response)` + +- 웹 브라우저 실행 + - http://localhost8080/hello?username=world + - 결과 : hello world + +- 콘솔 실행결과 +``` +HelloServlet.service +requset = org.apache.catalina.connector.RequestFacade@42a1a9b6 +response = org.apache.catalina.connector.ResponseFacade@e7a3bb +username = world +``` + + +### 서블릿 컨테이너 동작 방식 +**내장 톰캣 서버 생성** +![S2-1.png](img%2FS2-1.png) + +**HTTP 요청, HTTP응답 메시지** +![S2-2.png](img%2FS2-2.png) + +**웹 애플리케이션 서버의 요청 응답 구조** +![S2-3.png](img%2FS2-3.png) + +> **참고**
+> HTTP응답에서 Content-Length는 웹 애플리케이션 서버가 자동으로 생성해준다. + + +### welcome 페이지 추가 + +```html + + + + + Title + + + + + +``` + + +## HttpServletRequest - 개요 + +**HttpServletRequest 역할**
+HTTP 요청 메시지를 개발자가 직접 파싱해도 사용해도 되지만, 매우 불편하다. 서블릿은 개발자가 HTTP 요청 메시지를 +편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱한다. 그리고 그 결과를 `HttpServletRequest`객체에 담아서 제공한다. + +HttpServletRequest를 사용하면 다음과 같은 HTTP 요청 메시지를 편리하게 조회할 수 있다. + +*HTTP 요청 메시지* +``` +POST /save HTTP/1.1 +Host: localhost:8080 +Content-Type: application/x-www-form-urlencoded +username=kim&age=20 +``` + +- START LINE + - HTTP 메서드 + - URL + - 쿼리 스트링 + - 스키마, 프로토콜 +- 헤더 + - 헤더 조회 +- 바디 + - form 파라미터 형식 조회 + - message body 데이터 직접 조회 + +HttpServletRequest 객체는 추가로 여러가지 부가기능도 함께 제공한다. + +**임시 저장소 기능** +- 해당 HTTP 요청이 시작부터 끝날 때 까지 유지되는 임시 저장소 기능 + - 저장 : `request.setAttribute(name, value)` + - 조회 : `request.getAttribute(name)` + +**세션 관리 기능** +- `request.getSession(create: true)` + +> **중요**
+> HttpServletRequest, HttpServletResponse를 사용할 때 가장 중요한 점은 이 객체들이 HTTP 요청 메시지, +> HTTP 응답 메시지를 편리하게 사용하도록 도와주는 객체라는 점이다. 따라서 이 기능에 대해 깊이 이해하려면 +> **HTTP 스팩이 제공하는 요청, 응답 메시지 자체를 이해**해야 한다. + + + +## HttpServletRequest - 기본 사용법 +HttpServletRequest가 제공하는 기본 기능 + +*RequestHeaderServlet* +```java +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header") +public class RequestHeaderServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + printStartLine(request); + printHeaders(request); + printHeaderUtils(request); + printEtc(request); + + } + + //start line 정보 + private void printStartLine(HttpServletRequest request) { + System.out.println("--- REQUEST-LINE - start ---"); + System.out.println("request.getMethod() = " + request.getMethod()); //GET + System.out.println("request.getProtocol() = " + request.getProtocol()); //HTTP/1.1 + System.out.println("request.getScheme() = " + request.getScheme()); //http + // http://localhost:8080/request-header + System.out.println("request.getRequestURL() = " + request.getRequestURL()); + // /request-header + System.out.println("request.getRequestURI() = " + request.getRequestURI()); + //username=hi + System.out.println("request.getQueryString() = " + + request.getQueryString()); + System.out.println("request.isSecure() = " + request.isSecure()); //https 사용유무 + System.out.println("--- REQUEST-LINE - end ---"); + System.out.println(); + } + + //Header 모든 정보 + private void printHeaders(HttpServletRequest request) { + System.out.println("--- Headers - start ---"); + /* + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + System.out.println(headerName + ": " + request.getHeader(headerName)); + } + */ + request.getHeaderNames().asIterator() + .forEachRemaining(headerName -> System.out.println(headerName + ": " + + request.getHeader(headerName))); + System.out.println("--- Headers - end ---"); + System.out.println(); + } + + + //Header 편리한 조회 + private void printHeaderUtils(HttpServletRequest request) { + System.out.println("--- Header 편의 조회 start ---"); + System.out.println("[Host 편의 조회]"); + System.out.println("request.getServerName() = " + + request.getServerName()); //Host 헤더 + System.out.println("request.getServerPort() = " + + request.getServerPort()); //Host 헤더 + System.out.println(); + System.out.println("[Accept-Language 편의 조회]"); + request.getLocales().asIterator() + .forEachRemaining(locale -> System.out.println("locale = " + + locale)); + System.out.println("request.getLocale() = " + request.getLocale()); + System.out.println(); + System.out.println("[cookie 편의 조회]"); + if (request.getCookies() != null) { + for (Cookie cookie : request.getCookies()) { + System.out.println(cookie.getName() + ": " + cookie.getValue()); + } + } + System.out.println(); + System.out.println("[Content 편의 조회]"); + System.out.println("request.getContentType() = " + + request.getContentType()); + System.out.println("request.getContentLength() = " + + request.getContentLength()); + System.out.println("request.getCharacterEncoding() = " + + request.getCharacterEncoding()); + } + + //기타 정보 + private void printEtc(HttpServletRequest request) { + System.out.println("--- 기타 조회 start ---"); + System.out.println("[Remote 정보]"); + System.out.println("request.getRemoteHost() = " + + request.getRemoteHost()); // + System.out.println("request.getRemoteAddr() = " + + request.getRemoteAddr()); // + System.out.println("request.getRemotePort() = " + + request.getRemotePort()); // + System.out.println(); + System.out.println("[Local 정보]"); + System.out.println("request.getLocalName() = " + request.getLocalName()); // + System.out.println("request.getLocalAddr() = " + request.getLocalAddr()); // + System.out.println("request.getLocalPort() = " + request.getLocalPort()); // + System.out.println("--- 기타 조회 end ---"); + System.out.println(); + } +} +``` +실행결과 +``` +--- REQUEST-LINE - start --- +request.getMethod() = GET +request.getProtocol() = HTTP/1.1 +request.getScheme() = http +request.getRequestURL() = http://localhost:8080/request-header +request.getRequestURI() = /request-header +request.getQueryString() = null +request.isSecure() = false +--- REQUEST-LINE - end --- + +--- Headers - start --- +host: localhost:8080 +connection: keep-alive +cache-control: max-age=0 +sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120" +sec-ch-ua-mobile: ?0 +sec-ch-ua-platform: "Windows" +upgrade-insecure-requests: 1 +user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 +accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 +sec-fetch-site: same-origin +sec-fetch-mode: navigate +sec-fetch-user: ?1 +sec-fetch-dest: document +referer: http://localhost:8080/basic.html +accept-encoding: gzip, deflate, br +accept-language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7 +cookie: Idea-1afd2dea=715e6892-4a57-4307-afc0-f323498e55fb +--- Headers - end --- + +--- Header 편의 조회 start --- +[Host 편의 조회] +request.getServerName() = localhost +request.getServerPort() = 8080 + +[Accept-Language 편의 조회] +locale = ko_KR +locale = ko +locale = en_US +locale = en +request.getLocale() = ko_KR + +[cookie 편의 조회] +Idea-1afd2dea: 715e6892-4a57-4307-afc0-f323498e55fb + +[Content 편의 조회] +request.getContentType() = null +request.getContentLength() = -1 +request.getCharacterEncoding() = UTF-8 +--- 기타 조회 start --- +[Remote 정보] +request.getRemoteHost() = 0:0:0:0:0:0:0:1 +request.getRemoteAddr() = 0:0:0:0:0:0:0:1 +request.getRemotePort() = 57354 + +[Local 정보] +request.getLocalName() = 0:0:0:0:0:0:0:1 +request.getLocalAddr() = 0:0:0:0:0:0:0:1 +request.getLocalPort() = 8080 +--- 기타 조회 end --- +``` + +> **참고**
+> 로컬에서 테스트하면 IPv6정보가 나오는데, IPv4 정보를 보고싶다면 다음 옵션을 VM options에 넣어주면 된다. +>
`-Djava.net.preferIPv4Stack=true` + + +## HTTP 요청 데이터 - 개요 +HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법이 있다. + +**주로 다음 3가지 방법을 사용** +- **GET - 쿼리 파라미터** + - /url?username=seob&age=24 + - 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달 + - 예)검색, 필터, 페이징등등에서 많이 사용하는 방식 +- **POST - HTML Form** + - content-type : application/x-www-form-urlencoded + - 메시지 바디에 쿼리 파라미터 형식으로 전달 username=seob&age=24 + - 예)회원 가입, 상품 주문, HTML Form 사용 +- **HTTP message body**에 데이터를 직접 담아서 요청 + - HTTP API에서 주로 사용, JSON, XML, TEXT + - 데이터 형식은 주로 JSON에 사용 + - POST, PUT, PATCH + + +### HTTP 요청 데이터 - GET 쿼리 파라미터 + +전달 데이터 +- username=hello +- age=20 + +메시지 바디 없이, URL의 **쿼리 파라미터**를 사용해서 데이터 전달 +- 예)검색, 필터, 페이징등등에서 많이 사용하는 방식 + +쿼리 파라미터는 URL에 다음과 같이 `?`를 시작으로 보낼 수 있다. 추가 파라미터는 `&`로 구분 +- http://localhost:8080/request-param?username=hello&age=20 + +*RequestParamServlet* +```java +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Enumeration; + +@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param") +public class RequestParamServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException { + System.out.println("[전체 파라미터 조회] - start"); + +// Enumeration parameterNames = requset.getParameterNames(); + requset.getParameterNames().asIterator().forEachRemaining( + paramName -> System.out.println(paramName + "=" + requset.getParameter(paramName)) + ); + + System.out.println("[전체 파라미터 조회] - end"); + System.out.println(); + + System.out.println("[단일 파라미터 조회]"); + String username= requset.getParameter("username"); + String age = requset.getParameter("age"); + + System.out.println("username = " + username); + System.out.println("age = " + age); + + System.out.println("[이름이 같은 복수 파라미터 조회]"); + String[] usernames = requset.getParameterValues("username"); + for(String name: usernames) { + System.out.println("username = " + name); + } + } +} +``` +실행결과 - 파라미터 전송
+http://localhost:8080/request-param?username=hello&age=20 +``` +[전체 파라미터 조회] - start +username=hello +age=20 +[전체 파라미터 조회] - end + +[단일 파라미터 조회] +username = hello +age = 20 +[이름이 같은 복수 파라미터 조회] +username = hello +``` +
+ +실행결과 - 동일 파라미터 전송 +``` +[전체 파라미터 조회] - start +username=hello +age=20 +[전체 파라미터 조회] - end +[단일 파라미터 조회] +request.getParameter(username) = hello +request.getParameter(age) = 20 +[이름이 같은 복수 파라미터 조회] +request.getParameterValues(username) +username=hello +username=Lee +``` + +파라미터의 값은 하나인데, 값이 중복이면 `request.getParameterValue()` 를 사용해야한다.
+중복일 때 `request.getParameter()`를 사용하면 `request.getParameterValue()`의 첫 번째 값을 반환한다. + + +## HTTP 요청 데이터 - POST HTML FORM +주로 회원가입, 상품주문 등에서 사용 + +**특징** +- content-type : `application/x-www-form-urlencode` +- 메시지 바디에 쿼리 파라미터 형식으로 데이터를 전달. `username=hello&age=20` + +*hello-form.html* +```html + + + + + Title + + +
+ username: + age: + +
+ + +``` + +실행결과 + +![S2-4.png](img%2FS2-4.png) + +전송버튼 클릭 + +![S2-5.png](img%2FS2-5.png) + +POST의 HTML FORM을 전송하면 웹 브라우저는 다음 형식으로 HTTP 메시지를 만든다. +- **요청 URL** : http://localhost:8080/request-param +- **content-type** : `application/x-www-form-urlencoded` +- **message body** : `username=hello&age=20` + +`application/x-www-form-urlencoded`형식은 앞서 GET에서 살펴본 쿼리 파라미터 형식과 같다. +따라서 **쿼리 파라미터 조회 메서드를 그대로 사용**하면 된다. + +`request.getParameter()`는 GET URL 쿼리 파라미터 형식도 지원, POST HTML FORM 형식도 지원한다. + +> **참고**
+> content-type은 HTTP메시지 바디에 데이터 형식을 지정.
+> **GET URL 쿼리 파라미터 형식**으로 클라이언트에서 서버로 데이터를 전달할 때는 HTTP 메시지 바디를 사용하지 않기 때문에 content-type이 없다.
+> **POST FHTML Form 형식**으로 데이터를 전달하면 HTTP 메시지 바디에 해당 데이터를 포함해서 보내기 때문에 바디에 포함된 +> 데이터가 어떤 형식인지 content-type을 반드시 지정해야 한다. 이렇게 폼으로 데이터를 전송하는 형식을 `application/x-www-form-urlencoded`라 한다. + + +### Postman을 사용한 테스트 +Postman을 사용하면 굳이 HTML form 을 생성하지 않고 테스트 가능하다. +![S2-6.png](img%2FS2-6.png) + + +## HTTP 요청 데이터 - API 메시지 바디 - 단순 텍스트 +- **HTTP message body**에 데이터를 직접 담아서 요청 + - HTTP API에서 주로 사용, JSON, XML, TEXT + - 데이터 형식은 주로 JSON사용 + - POST, PUT, PATCH + + +- 먼저 가장 단순한 텍스트 메시지를 HTTP 메시지에 담아 전송하고 읽기. +- HTTP 메시지 바디의 데이터를 InputStream을 사용해 직접 읽을 수 있다. + +*RequestBodyStringServlet* +```java +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@WebServlet(name="RequestBodyStringServlet", urlPatterns = "/request-body-string") +public class RequestBodyStringServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + + System.out.println("messageBody = " + messageBody); + + response.getWriter().write("OK"); + } +} +``` + +Postman 테스트 + +![S2-7.png](img%2FS2-7.png) + +문자 전송 +- POST http://localhost:8080/request-body-string +- content-type: text/plain +- message body: hello + +실행결과 +``` +messageBody = helloooo! +``` + +> **참고**
+> inputStream은 byte코드로 반환. byte코드를 우리가 읽을 수 있는 문자(String)로 보려면 +> 문자표(Charset)를 지정해야함. 여기서 UTF_8을 지정 + + + +## HTTP 요청 데이터 - API 메시지 바디 - JSON +HTTP API에서 주로 사용하는 JSON형식으로 데이터 전달해보기 + +**JSON 형식 전송** +- content-type : **application/json** +- message body : `{"username": "hello","age": 20}` +- 결과 : `messageBody = {"username":"hello","age":20}` + +**JSON 형식 바싱 추가** + +*HelloData* +```java +package hello.servlet.basic; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class HelloData { + private String username; + private int age; +} +``` + +*RequestBodyJsonServlet* +```java +package hello.servlet.basic.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import hello.servlet.basic.HelloData; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json") +public class RequestBodyJsonServlet extends HttpServlet { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + + System.out.println("messageBody = " + messageBody); + + HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); + + System.out.println("helloData.username = " + helloData.getUsername()); + System.out.println("helloData.age = " + helloData.getAge()); + + response.getWriter().write("ok"); + } +} + +``` +Postman으로 실행 + +![S2-8.png](img%2FS2-8.png) + +실행결과 +``` +messageBody = {"username": "hello","age": 20} +helloData.username = hello +helloData.age = 20 +``` + +> **참고**
+> JSON결과를 파싱해서 사용할 수 있는 자바 객체로 변환하려면 Jackson, Gson 같은 JSON 변환 라이브러리를 추가해서 사용해야한다. +> Spring MVC를 선택하면 기본으로 Jackson 라이브러리(ObjectMapper)를 함께 제공한다. + + +> **참고**
+> HTML form 데이터도 메시지 바디를 통해 전송되므로 직접 읽을 수 있다. 하지만 편리한 파라미터 조회기능 +> (request.getParameter(...))을 이미 제공하기 때문에 파라미터 조회 기능을 사용하면 된다. + + + +## HttpServletResponse - 기본 사용법 +### HttpServletResponse 역할 + +**HTTP 응답 메시지 생성** +- HTTP 응답 코드 지정 +- 헤더 생성 +- 바디 생성 + +**편의 기능 제공** +- Content-Type, 쿠키, Redirect + + +### HttpServletResponse - 기본 사용법 + +*ResponseHeaderServlet* +```java +package hello.servlet.basic.response; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header") +public class ResponseHeaderServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + //[status-line] + response.setStatus(HttpServletResponse.SC_OK); + + //[response-headers] + response.setHeader("Content-Type", "text/plain;charset=utf-8"); + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("my-header", "hello"); + + //[Header 편의 메서드] +// content(response); +// cookie(response); +// redirect(response); + + //[message body] + PrintWriter writer = response.getWriter(); + writer.println("ok"); + } +} +``` + +*Content 편의 메서드* +```java + private void content(HttpServletResponse response) { + //Content-Type: text/plain;charset=utf-8 + //Content-Length: 2 + //response.setHeader("Content-Type", "text/plain;charset=utf-8"); + response.setContentType("text/plain"); + response.setCharacterEncoding("utf-8"); + //response.setContentLength(2); //(생략시 자동 생성) + } +``` + +*Cookie 편의 메서드* +```java + private void cookie(HttpServletResponse response) { + //Set-Cookie: myCookie=good; Max-Age=600; + //response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600"); + Cookie cookie = new Cookie("myCookie", "good"); + cookie.setMaxAge(600); //600초 + response.addCookie(cookie); + } +``` + +*Redirect 편의 메서드* +```java + private void redirect(HttpServletResponse response) throws IOException { + //Status Code 302 + //Location: /basic/hello-form.html + //response.setStatus(HttpServletResponse.SC_FOUND); //302 + //response.setHeader("Location", "/basic/hello-form.html"); + response.sendRedirect("/basic/hello-form.html"); + } +``` + + +## HTTP 응답 데이터 - 단순 텍스트, HTML +HTTP 응답 메시지는 주로 아래와 같은 내용을 담아 전달 +- 단순 텍스트 응답 + - `writer.println("ok");` +- HTML 응답 +- HTML API - MessageBody JSON 응답 + +### HttpServletResponse - HTML 응답 + +*ResponseHtmlServlet* +```java +package hello.servlet.basic.response; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html") +public class ResponseHtmlServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + //Content-Type: text/html;charset=utf-8 + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + PrintWriter writer = response.getWriter(); + writer.println(""); + writer.println(""); + writer.println("
HI?
"); + writer.println(""); + writer.println(""); + } +} +``` + +HTTP 응답으로 HTML을 반환할 때 content-type을 `text/html`로 지정해야 한다. + + +## HTTP 응답 데이터 - API JSON + + +*ResponseJsonServlet* +```java +package hello.servlet.basic.response; + +import com.fasterxml.jackson.databind.ObjectMapper; +import hello.servlet.basic.HelloData; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json") +public class ResponseJsonServlet extends HttpServlet { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + + HelloData helloData = new HelloData(); + helloData.setUsername("lee"); + helloData.setAge(20); + + String result = objectMapper.writeValueAsString(helloData); + response.getWriter().write(result); + } +} +``` + +HTTP 응답으로 JSON을 반환할 때 content-type을 `application/json`로 지정해야 한다.
+Jackson 라이브러리가 제공하는 `objectMapper.writeValueAsString()`를 사용하면 객체를 JSON 문자로 변경할 수 있다. + + diff --git a/9 WEEK/servlet/SECTION3.md b/9 WEEK/servlet/SECTION3.md new file mode 100644 index 00000000..5f56ad9a --- /dev/null +++ b/9 WEEK/servlet/SECTION3.md @@ -0,0 +1,803 @@ +# 3. 서블릿, JSP, MVC 패턴 + + +## 회원 관리 웹 애플리케이션 요구사항 + +회원정보 - 이름, 나이
+기능 요구사항 - 회원 저장, 회원 목록 조회 + +*Member* +```java +package hello.servlet.domain.member; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Member { + + private Long id; + private String username; + private int age; + + public Member(String username, int age) { + this.username = username; + this.age = age; + } +} +``` + + +*MemberRepository* +```java +package hello.servlet.domain.member; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MemberRepository { + + private static Map store = new HashMap<>(); + private static long sequence = 0L; + + //싱글톤 + private static final MemberRepository instance = new MemberRepository(); + + public static MemberRepository getInstance() { + return instance; + } + private MemberRepository(){} + + public Member save(Member member) { + member.setId(++sequence); + store.put(member.getId(), member); + return member; + } + + public Member findById(Long id) { + return store.get(id); + } + + public List findAll() { + return new ArrayList<>(store.values()); + } + + public void clearStore() { + store.clear(); + } +} +``` + +싱글톤 패턴 적용. 싱글톤 패턴은 객체를 단 하나만 생성해서 공유해야 하므로 생성자를 private 접근자로 막아둔다. + + +*MemberRepositoryTest* +```java +package hello.servlet.domain.member; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MemberRepositoryTest { + + MemberRepository memberRepository = MemberRepository.getInstance(); + + @AfterEach + void afterEach() { + memberRepository.clearStore(); + } + + @Test + void save() { + Member member = new Member("Hello", 20); + + Member savedMember = memberRepository.save(member); + + Member findMember = memberRepository.findById(savedMember.getId()); + Assertions.assertThat(findMember).isEqualTo(savedMember); + + } + + @Test + void findAll() { + Member member1 = new Member("Hello1", 20); + Member member2 = new Member("Hello2", 20); + + memberRepository.save(member1); + memberRepository.save(member2); + + List memberList = memberRepository.findAll(); + + Assertions.assertThat(memberList.size()).isEqualTo(2); + Assertions.assertThat(memberList).contains(member1, member2); + } + + +} +``` + + +## 서블릿으로 회원 관리 웹 애플리케이션 만들기 + +*MemberFormServlet - 회원 등록 폼* +```java +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form") +public class MemberFormServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + + PrintWriter w = response.getWriter(); + + w.write("\n" + + "\n" + + "\n" + + " \n" + + " Title\n" + + "\n" + + "\n" + + "
\n" + + " username: \n" + + " age: \n" + + " \n" + + "
\n" + + "\n" + + "\n"); } +} +``` + + +*MemberSaveServlet - 회원 저장* +```java +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save") +public class MemberSaveServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("MemberSaveServlet.service"); + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + PrintWriter w = response.getWriter(); + + w.write("\n" + + "\n" + + " \n" + + "\n" + + "\n" + + "성공\n" + + "
    \n" + + "
  • id="+member.getId()+"
  • \n" + + "
  • username="+member.getUsername()+"
  • \n" + + "
  • age="+member.getAge()+"
  • \n" + + "
\n" + + "메인\n" + + "\n" + + ""); + } +} +``` + +- `MemberSaveServlet` 동작 순서 + 1. 파라미터를 조회해 Member 객체 생성 + 2. Member 객체를 MemberRepository를 통해 저장. + 3. Member 객체를 사용해 결과 화면용 HTML을 동적으로 만들어 응답. + + + +*MemberListServlet - 회원 목록* +```java +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members") +public class MemberListServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + List members = memberRepository.findAll(); + + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + + PrintWriter w = response.getWriter(); + w.write(""); + w.write(""); + w.write(" "); + w.write(" Title"); + w.write(""); + w.write(""); + w.write("메인"); + w.write(""); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); +/* + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); +*/ + for (Member member : members) { + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + } + w.write(" "); + w.write("
idusernameage
1userA10
" + member.getId() + "" + member.getUsername() + "" + member.getAge() + "
"); + w.write(""); + w.write(""); + } +} +``` + +- `MemberListServlet` 동작 순서 + 1. `memberRepository.findAll()`을 통해 모든 회원을 조회. + 2. 회원 목록 HTML을 for 루프를 통해 회원 수 만큼 동적으로 생성하고 응답. + + + +### Welcome 페이지 변경 + +*index.html* +```html + + + + + Title + + + + + +``` + + + + +## JSP로 회원 관리 웹 애플리케이션 만들기 + +### JSP 라이브러리 추가 + +*build.gradle* +```groovy + //JSP 추가 시작 +implementation 'org.apache.tomcat.embed:tomcat-embed-jasper' +implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상 +implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //스프링부트3.0 이상 +implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상 +//JSP 추가 끝 +``` + +*new-form.jsp - 회원 등록 폼* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Title + + +
+ username: + age: + +
+ + +``` + +- `<%@ page contentType="text/html;charset=UTF-8" language="java" %>` + - 첫 줄은 JSP 문서라는 뜻. + +*save.jsp - 회원 저장* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="hello.servlet.domain.member.Member" %> +<%@ page import="hello.servlet.domain.member.MemberRepository" %> +<% + MemberRepository memberRepository = MemberRepository.getInstance(); + //request, response는 지원함 + System.out.println("MemberSaveServlet.service"); + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); +%> + + + Title + + + 성공 +
    +
  • id=<%=member.getId()%>
  • +
  • username=<%=member.getUsername()%>
  • +
  • age=<%=member.getAge()%>
  • +
+ 메인 + + +``` + +- '<%@ page import= %>' + - 자바의 import 문과 동일. +- '<% %>' + - 이 부분에 자바 코드 입력 가능. +- `<%= %>` + - 이 부분에 자바 코드 출력 가능. + +회원 저장 JSP는 회원 저장 servlet 코드와 같다. 다른 점은, HTML을 중심으로 하고, +자바 코드를 부분부분 입력해 주었다는 점이다. `<% %>`를 사용해 HTML 중간에 자바 코드를 출력하고 있다. + +*members.jsp - 회원 목록* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="hello.servlet.domain.member.Member" %> +<%@ page import="java.util.List" %> +<%@ page import="hello.servlet.domain.member.MemberRepository" %> +<% + MemberRepository memberRepository = MemberRepository.getInstance(); + + List members = memberRepository.findAll(); +%> + + + + Title + + +메인 + + + + + + + + <% + for (Member member : members) { + out.write(" "); + out.write(" "); + out.write(" "); + out.write(" "); + out.write(" "); + } + %> + +
idusernameage
" + member.getId() + "" + member.getUsername() + "" + member.getAge() + "
+ + +``` + +회원 repository를 먼저 조회, 결과 List를 사용해 중간에 ``HTML 태그를 반복해서 출력하고 있다. + + +### 서블릿과 JSP의 한계 +서블릿으로 개발할 때는 뷰(view)화면을 위한 HTML을 만드는 작업이 자바 코드에 섞여 복잡했다.
+JSP를 사용한 덕분에 뷰를 생성하는 HTML 작업을 깔끔하게 정리하고, 중간중간 동적으로 변경이 필요한 부분에만 +자바 코드를 적용했다. 하지만 몇 가지 문제점이 존재한다. + +회원 저장 폼에서 코드의 상위 절반은 회원을 저장하는 비지니스 로직, 나머지 절반은 결과를 보여주는 HTML 뷰 영역이다. 회원 목록도 마찬가지이다.
+JAVA코드, 데이터를 조회하는 repository 등등 다양한 코드가 모두 JSP에 노출돼 있다. 만약 수백, 수천줄이 넘어간다면 유지보수에 큰 어려움이 발생한다. + +
+**MVC 패턴 등장** +
비지니스 로직은 서블릿 처럼 다른곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면(VIEW)을 보여주는 일에 집중하도록 하게 해준다. + + + + +## MVC 패턴 -적용 +서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해 MVC 패턴 적용 +
Model은 HttpServletRequest 객체 사용. request는 내부에 데이터 저장소를 가지고 있는데, +'request.setAttribute()', `request.getAttribute`를 사용하면 데이터를 보관하고, 조회할 수 있다. + + +### 회원 등록 + +*MvcMemberFormServlet - 회원 등록 폼 컨트롤러* +```java +package hello.servlet.web.servletmvc; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form") +public class MvcMemberFormServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String viewPath = "/WEB-INF/views/new-form.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + + } +} +``` + +`dispatcher.forward()` : 다른 서블릿이나 JSP로 이동할 수 있는 기능. 서버 내부에서 다시 호출 발생 + +> `/WEB-INF`
+> 이 경로 안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 즉 항상 컨트롤러를 통해서 JSP를 호출하는 것이다. + +> **redirect vs forward**
+> 리다이렉트는 실제 클라이언트(web)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다. +> 따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다. 반면에 포워드는 서버 내부에서 +> 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다. + + +*new-form.jsp - 회원 등록 폼 뷰* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + Title + + + +
+ username: + age: + +
+ + +``` + +여기서 form의 action이 상대경로로 지정돼 있다. 이렇게 상대경로로 지정하면 폼 전송시 +현재 URL이 속한 계층 경로 + save가 호출된다.
+현재 계층 경로 : `/servlet-mvc/members/`
+결과 : `/servlet-mvc/members/save` + + +### 회원 저장 + +*MvcMemberSaveServlet - 회원 저장 컨트롤러* +```java +package hello.servlet.web.servletmvc; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save") +public class MvcMemberSaveServlet extends HttpServlet { + + MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + + //Model에 데이터 보관. + request.setAttribute("member", member); + + String viewPath = "/WEB-INF/views/save-result.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} +``` +HttpServletRequest를 Model로 사용.
+request가 제공하는 setAttribute()를 사용하면 request 객체에 데이터를 보관해 뷰에 전달할 수 있다.
+뷰는 request.getAttribute()를 사용해 데이터를 꺼내면 된다. + + +*save-result - 회원저장 뷰* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + + +성공 +
    +
  • id=${member.id}
  • +
  • username=${member.username}
  • +
  • age=${member.age}
  • +
+메인 + + +``` + +JSP는 `${}` 문법을 제공. 이 문법을 사용하면 request의 attribute에 담긴 데이터를 편리하게 조회할 수 있다. + + +### 회원 목록 조회 + +*MvcMemberListServlet - 회원 목록 조회 컨트롤러* +```java +package hello.servlet.web.servletmvc; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members") +public class MvcMemberListServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + + request.setAttribute("members", members); + + String viewPath = "/WEB-INF/views/members.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} +``` + +request 객체를 사용해 `List members`를 모델에 보관한다. + + +*members - 회원 목록 조회 뷰* +```html +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> + + + + Title + + +메인 + + + + + + + + + + + + + + + +
idusernameage
${item.id}${item.username}${item.age}
+ + +``` +모델에 담아둔 members를 JSP가 제공하는 taglib기능을 사용해 반복해서 출력한다. +`memebers`리스트에서 `members`를 순서대로 꺼내 `item`변수에 담고, 출력하는 과정을 반복한다. +

+``이 기능을 사용하려면 다음과 같이 선언해야 한다.
+`<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>` + + + +## MVC 패턴 - 한계 +MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다.
+뷰는 화면을 그리는 역할에 충실한 덕분에, 코드가 깔끔하고 직관적이다. 단순하게 모델에서 필요한 데이터를 수집하고, 화면을 만들면 된다. +
+하지만 컨트롤러는 중복코드가 많고, 필요하지 않은 코드가 많이 존재한다. + + +**MVC 컨트롤러의 단점**
+ +*foward 중복*
+view로 이동하는 코드가 항상 중복 호출된다. +``` +RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); +dispatcher.forward(request, response); +``` + + +**ViewPath 중복** +```java +String viewPath = "/WEB-INF/views/save-result.jsp"; +``` + +- prefix : `/WEB-INF/views/` +- suffix : `.jsp`
+그리고 만약 jsp가 아닌 thymeleaf같은 뷰로 변경한다면 전체 코드를 다 변경 작업을 해야 한다. + + +**사용하지 않는 코드**
+다음 코드를 사용할 때도 있고, 사용하지 않을 때도 있다. +``` +HttpServletRequest request, HttpServletResponse response +``` +그리고 이런 `HttpServletRequest`, `HttpServletResponse`를 사용하는 코드는 테스트 케이스를 작성하기 어렵다. + + +**공통 처리 어려움**
+기능이 복잡해질수록 컨트롤러에서 공통으로 처리해야 하는 부분이 점점 더 많이 증가한다. 단순히 공통 기능을 메서드로 +생성하면 될 것 같지만, 결과적으로 해당 메서드를 항상 호출해야 하고, 실수로 호출하지 않으면 문제가 될 수도 있다. 그리고 호출하는 것 자체도 중복이다. + + +**정리!**
+이 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다. 소위 **수문장 역할**을 하는 기능이 필요하다. +**Front Controller**패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다.(입구를 하나로)
+스프링 MVC의 핵심도 바로 이 프론트 컨트롤러에 있다. + + + + + + + + + + + + + + + + + diff --git a/9 WEEK/servlet/SECTION4.md b/9 WEEK/servlet/SECTION4.md new file mode 100644 index 00000000..f27d814e --- /dev/null +++ b/9 WEEK/servlet/SECTION4.md @@ -0,0 +1,1251 @@ +# 4. MVC 프레임워크 만들기 + +--- + +## 프론트 컨트롤러 패턴 + +**프론트 컨트롤러 도입 전** +![S4-1.png](img%2FS4-1.png) +

+**프론트 컨트롤러 도입 후** +![S4-2.png](img%2FS4-2.png) + + +**FrontController 패턴 특징** +- 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음 +- 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아 호출 +- 입구를 하나로 +- 공통 처리 가능 +- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨 + + +**스프링 웹MVC와 프론트 컨트롤러** +- 스프링 웹 MVC의 핵심도 바로 **FrontController** +- 스프링 웹 MVC의 **DispatcherServlet**이 FrontController 패턴으로 구현되어 있다. + + + +## 프론트 컨트롤러 도입 - v1 + +V1 구조 +![S4-3.png](img%2FS4-3.png) + + +*ControllerV1* +```java +package hello.servlet.web.frontcontroller.v1; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface ControllerV1 { + void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; +} +``` + +서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입.
+각 컨트롤러들은 이 인터페이스를 구현
+프론트 컨트롤러는 이 인터페이스를 호출해 구현과 관계 없이 로직의 일관성을 가질 수 있음. + + +*MemberFormControllerV1 - 회원 등록 컨트롤러* +```java +package hello.servlet.web.frontcontroller.v1.controller; + +import hello.servlet.web.frontcontroller.v1.ControllerV1; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberFormControllerV1 implements ControllerV1 { + @Override + public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String viewPath = "/WEB-INF/views/new-form.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} +``` + +*MemberListControllerV1 - 회원 목록 컨트롤러* +```java +package hello.servlet.web.frontcontroller.v1.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.v1.ControllerV1; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +public class MemberListControllerV1 implements ControllerV1 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + + request.setAttribute("members", members); + + String viewPath = "/WEB-INF/views/members.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} +``` +내부 로직은 기존 서블릿과 비슷 + + +*FrontControllerServletV1 - 프론트 컨트롤러* +```java +package hello.servlet.web.frontcontroller.v1; + +import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1; +import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1; +import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*") +public class FrontControllerServiceV1 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV1() { + controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1()); + controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1()); + controllerMap.put("/front-controller/v1/members", new MemberListControllerV1()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV1 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + controller.process(request, response); + } +} +``` + +**프론트 컨트롤러 분석**

+ +**urlPatterns** +- `urlPatterns = "/front-controller/v1/*"` : `/front-controller/v1/*`를 포함한 하위 모든 요청은 +이 서블릿에서 받아들인다. +- Ex) `/front-controller/v1`, `/front-cotroller/v1/a`, `/front-controller/v1/a/b` + + +**controllerMap** +- key : 매핑 URL +- value : 호출된 컨트롤러 + + +**service()**
+먼저 `requestURI`를 조회해 실제 호출할 컨트롤러를 controllerMap에서 찾는다. 없다면 404 상태 코드 반환한다. +
컨트롤러를 찾고 `controller.process(request, response);`을 호출해서 해당 컨트롤러를 실행한다. + + +**JSP**
+JSP는 이전 MVC에서 사용했던 파일을 그대로 사용 +

+ + +**기존 서블릿, JSP로 만든 MVC와 동일하게 실행된다.** + + + + + +## View 분리 - v2 +모든 컨트롤러에서 뷰로 이동한느 부분에 중복 발생, 깔끔하지 않음 +``` + String viewPath = "/WEB-INF/views/members.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); +``` + +이를 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 만든다. + +*V2 구조* +![S4-4.png](img%2FS4-4.png) + +이전 V1 구조에서는 controller에서 jsp를 호출했다면 V2는 view를 반환해 FrontController에서 실행하게 한다. + + +*MyView* +```java +package hello.servlet.web.frontcontroller; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MyView { + private String viewPath; + + public MyView(String viewPath) { + this.viewPath = viewPath; + } + + public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} +``` + +*ControllerV2* +```java +package hello.servlet.web.frontcontroller.v2; + +import hello.servlet.web.frontcontroller.MyView; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface ControllerV2 { + MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; +} +``` + + +*MemberFormControllerV2 - 회원 등록 폼* +```java +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberFormControllerV2 implements ControllerV2 { + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + return new MyView("/WEB-INF/views/new-form.jsp"); + } +} + +``` + +각 컨트롤러는 복잡한 dispatcher.forward()를 직접 생성해 호출하지 않아도 됨. +단순히 MyView 객체를 생성, 거기에 뷰 이름만 넣고 반환하면 됨. + +ControllerV1을 구현한 클래스와 ControllerV2를 구현한 클래스를 비교하면, 중복이 확실하게 제거됨을 확인 가능함 + +*MemberSaveControllerV2 - 회원 저장* +```java +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberSaveControllerV2 implements ControllerV2 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + request.setAttribute("member", member); + return new MyView("/WEB-INF/views/save-result.jsp"); + } +} +``` + + +*MemberListControllerV2 - 회원 목록* +```java +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +public class MemberListControllerV2 implements ControllerV2 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + request.setAttribute("members", members); + return new MyView("/WEB-INF/views/members.jsp"); + } +} +``` + +*FrontControllerV2* +```java +package hello.servlet.web.frontcontroller.v2; + +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*") +public class FrontControllerServiceV2 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV2() { + controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2()); + controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2()); + controllerMap.put("/front-controller/v2/members", new MemberListControllerV2()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV2 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + MyView view = controller.process(request, response); + view.render(request, response); + } +} +``` + +ControllerV2의 반환 타입이 `MyView`이므로 프론트 컨트롤러는 컨트롤러의 호출 결과를 `MyView`를 반환 받는다. +그리고 `view.render()`를 호출하면 `forward` 로직을 수행해 JSP가 실행된다. + +`MyView.render()` +```java + public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +``` + + + + +## Model 추가 - v3 +**서블릿 종속 제거**
+컨트롤러 입장에서 HttpServletRequest, HttpServletResponse가 꼭 필요할까?
+요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 지금 구조에서 컨트롤러가 서블릿 기술을 몰라도 동작 할 수 있다. +또한 request 객체를 Model로 사용하는 대신 별도의 Model 객체를 만들어 반환하면 된다.
+구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않고 변경 ->> + +**뷰 이름 종속 제거**
+컨트롤러에서 지정하는 뷰 이름에 중복이 있는 것을 확인할 수 있다.
+컨트롤러의 *뷰의 논리 이름*을 반환, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화.
+이렇게 하면 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다. + +- `/WEB-INF/views/new-form.jsp` -> **new-form** +- `/WEB-INF/views/save-result.jsp` -> **save-result** +- `/WEB-INF/views/members.jsp` -> **members** + + +**V3 구조** +![S4-5.png](img%2FS4-5.png) + + +**Model View**
+지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다. 그리고 Model도 `request.setAttribute()`를 통해 데이터를 저장, 뷰에 전달했다. +
서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View이름가지 전달하는 객체를 만들어본다. +
참고로 `ModelView`객체는 다른 버전에서도 사용하므로 패키지를 `frontController`에 둔다. + + +*ModelView* +```java +package hello.servlet.web.frontcontroller; + +import java.util.HashMap; +import java.util.Map; +public class ModelView { + private String viewName; + private Map model = new HashMap<>(); + public ModelView(String viewName) { + this.viewName = viewName; + } + public String getViewName() { + return viewName; + } + public void setViewName(String viewName) { + this.viewName = viewName; + } + public Map getModel() { + return model; + } + public void setModel(Map model) { + this.model = model; + } +} +``` +뷰의 이름과 뷰를 렌더링할 때 필요한 model 객체를 가지고 있음. model은 단순히 map으로 되어 있어 컨트롤러에서 뷰에 필요한 데이터를 +key, value로 넣어주면 됨. + + +*ControllerV3* +```java +package hello.servlet.web.frontcontroller.v3; + +import hello.servlet.web.frontcontroller.ModelView; + +import java.util.Map; + +public interface ControllerV3 { + ModelView process(Map paramMap); +} +``` +이 컨트롤러는 서블릿 기술을 사용하지 않음. 따라서 구현이 단순해지고, 테스트 코드를 작성하기 쉬움
+HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아 호출하면 됨
+응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체를 반환하면 됨. + + +*MemberFormControllerV3 - 회원 등록 폼* +```java +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.Map; + +public class MemberFormControllerV3 implements ControllerV3 { + @Override + public ModelView process(Map paramMap) { + return new ModelView("new-form"); + } +} +``` +`ModelView`를 생성할 때 `new-form`이라는 view의 논리적인 이름을 지정. 실제 물리적은 이름은 프론트 컨트롤러에서 처리한다. + + +*MemberSaveControllerV3 - 회원 저장* +```java +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.Map; + +public class MemberSaveControllerV3 implements ControllerV3 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public ModelView process(Map paramMap) { + String username = paramMap.get("username"); + int age = Integer.parseInt(paramMap.get("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + ModelView mv = new ModelView("save-result"); + mv.getModel().put("member", member); + return mv; + } +} +``` + +- `paramMap.get("username");` + - 파라미터 정보는 map에 담겨있음. map에서 필요한 요청 파라미터를 조회하면 됨. +- `mv.getModel().put("member", member);` + - 모델은 단순한 map이므로 모델에 뷰에서 필요한 `member`객체를 담고 반환 + + +*MemberListControllerV3 - 회원 목록* +```java +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.List; +import java.util.Map; + +public class MemberListControllerV3 implements ControllerV3 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public ModelView process(Map paramMap) { + List members = memberRepository.findAll(); + ModelView mv = new ModelView("members"); + mv.getModel().put("members", members); + + return mv; + + } + +} +``` + +*FrontControllerServletV3* +```java +package hello.servlet.web.frontcontroller.v3; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2; +import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*") +public class FrontControllerServiceV3 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV3() { + controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3()); + controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3()); + controllerMap.put("/front-controller/v3/members", new MemberListControllerV3()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV3 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + Map paramMap = createParamMap(request); + ModelView mv = controller.process(paramMap); + String viewName = mv.getViewName(); + MyView view = viewResolver(viewName); + view.render(mv.getModel(), request, response); + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} +``` + + +`view.render(mv.getModel(), request, response)`코드에서 컴파일 오류가 발생함. 다음 코드를 참고해 MyView 객체에 필요한 메서드 추가. + +`createParamMap()`
+HttpServletRequest에서 파라미터 정보를 꺼내 Map으로 변환. 그리고 해당 Map(`paramMap`)을 컨트롤러에 전달하면서 호출 + + +**View Resolver** +`MyView view = viewResolver(viewName)`
+컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다. 그리고 실제 물리 경로가 있는 MyView 객체를 반환 +한다. +- 논리 뷰 이름 : `members` +- 물리 뷰 경로 : `/WEB-INF/views/members.jsp` + + + +`view.render(mv.getModel(), request, response)` +- 뷰 객체를 통해서 HTML 화면을 렌더링 한다. +- 뷰 객체의 render() 는 모델 정보도 함께 받는다. +- JSP는 request.getAttribute() 로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내서 request.setAttribute() 로 담아둔다. +- JSP로 포워드 해서 JSP를 렌더링 한다. + + +*MyView* +```java +package hello.servlet.web.frontcontroller; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Map; + +public class MyView { + private String viewPath; + + public MyView(String viewPath) { + this.viewPath = viewPath; + } + public void render(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } + public void render(Map model, HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + modelToRequestAttribute(model, request); + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } + private void modelToRequestAttribute(Map model, + HttpServletRequest request) { + model.forEach((key, value) -> request.setAttribute(key, value)); + } +} +``` + + + +## 단순하고 실용적인 컨트롤러 - v4 +앞서 만든 v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다. +하지만 실제 컨트롤러 인터페이스를 구현하는 개발자 입장에서 항상 ModelView 객체를 생성하고 반환해야하는 번거러움이 존재한다. +
좋은 프레임 워크는 아키텍쳐도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다. 즉 실용성이 있어야 한다. + + +v4는 v3를 약간 변경해 실제 구현하는 개발자들이 매우 편리하게 개발할 수 있도록 한다. + + +**V4 구조** +![S4-6.png](img%2FS4-6.png) +- 기본적인 구조는 V3와 같다. 대신에 컨트롤러가 ModelView를 반환하지 않고, ViewName만 반환한다. + + +*ControllerV4* +```java +package hello.servlet.web.frontcontroller.v4; + +import java.util.Map; + +public interface ControllerV4 { + String process(Map paramMap, Map model); +} +``` +이번 V4는 interface에 ModelView가 없다. model 객체는 파라미터로 전달되기 때문에 그냥 사용하면 되고, 결과로 뷰의 이름만 반환하면 된다. + + +*MemberFormControllerV4* +```java +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.Map; + +public class MemberFormControllerV4 implements ControllerV4 { + @Override + public String process(Map paramMap, Map model) { + return "new-form"; + } +} +``` + + +*MemberSaveControllerV4* +```java +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.Map; + +public class MemberSaveController implements ControllerV4 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public String process(Map paramMap, Map model) { + String username = paramMap.get("username"); + int age = Integer.parseInt(paramMap.get("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + model.put("member", member); + + return "save-result"; + } +} +``` +`model.put("member", member)` : 모델이 파라미터로 전달되기 때문에 모델을 직접 생성하지 않아도 된다. + + + +*MemberListControllerV4* +```java +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.List; +import java.util.Map; + +public class MemberListControllerV4 implements ControllerV4 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public String process(Map paramMap, Map model) { + List members = memberRepository.findAll(); + + model.put("members", members); + return "members"; + } +} +``` + + +*FrontControllerServletV4* +```java +package hello.servlet.web.frontcontroller.v4; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*") +public class FrontControllerServiceV4 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV4() { + controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4()); + controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4()); + controllerMap.put("/front-controller/v4/members", new MemberListControllerV4()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV4 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + Map paramMap = createParamMap(request); + Map model = new HashMap<>(); + + String viewName = controller.process(paramMap, model); + + MyView view = viewResolver(viewName); + view.render(model, request, response); + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} +``` +`FrontControllerServletV4`는 이전 버전과 거의 동일 +
+ +**모델 객체 전달**
+`Map model = new HashMap<>();`
+모델 객체를 프론트 컨트롤러에서 생성해 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다. + + +**뷰의 논리 이름을 직접 반환**
+```java +String viewName = controller.process(paramMap, model); +MyView view = viewResolver(viewName); +``` +컨트롤러가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해 실제 물리 뷰를 찾을 수 있다. + +**정리** +V4는 매우 단순하고 실용적이다. 기존 구조에서 모델을 파라미터로 넘기고, 뷰의 논리 이름을 반환한다는 작은 아이디어를 적용했을 뿐인데, 컨트롤러를 구현하는 +개발자 입장에서 군더더기 없는 코드를 작성할 수 있다.
+ + + + + +## 유연한 컨트롤러1 - V5 +만약 `ControllerV3` or `ControllerV4` 방식으로 다양한 컨트롤러를 사용해 개발하고 싶다면 **어댑터 패턴을** +을 사용해야 한다. + +**어댑터 패턴** +지금까지 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있었다.
+`ControllerV3`, `ControllerV4`는 완전히 다른 인터페이스이다. 따라서 호환이 불가능하다. 마치 v3는 110v, v4는 220v 전기 콘셉트 같은 것이다. +
어댑터 패턴을 사용해 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경한다. + +**V5 구조** +![S4-7.png](img%2FS4-7.png) +- **핸들러 어댑터** : 중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름은 어댑터 핸들러이다. +여기서 어댑터 역할을 해주기 때문에 다양한 종료의 컨트롤러를 호출할 수 있다. +- **핸들러** : 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 이제 어뎁터가 있기 때문이다.
+꼭 컨트롤러의 개념 뿐 아니라 어떠한 것이든 해당하는 종료의 어댑터만 있으면 다 처리할 수 있기 때문이다. + + +*MyHandlerAdapter* 인터페이스 +```java +package hello.servlet.web.frontcontroller.v5; + +import hello.servlet.web.frontcontroller.ModelView; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface MyHandlerAdapter { + boolean supports(Object handler); + + ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException; +} +``` +- `boolean supports(Object handler)` + - handler는 컨트롤러를 말함 + - 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 Method. +- `ModelView handle(HttpServletRequest request, HttpServletResponse response, Object Handler)` + - 어댑터는 실제 컨트롤러를 호출, 그 결과로 ModelView를 반환해야 함. + - 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 함. + - 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만, 이제는 이 어댑터를 통해 실제 컨트롤러가 호출됨. + + +실제 ControllerV3를 지원하는 어댑터를 구현
+*ControllerV3HandlerAdapter* +```java +package hello.servlet.web.frontcontroller.v5.adapter; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; +import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; +public class ControllerV3HandlerAdapter implements MyHandlerAdapter { + @Override + public boolean supports(Object handler) { + return (handler instanceof ControllerV3); + } + @Override + public ModelView handle(HttpServletRequest request, HttpServletResponse + response, Object handler) { + ControllerV3 controller = (ControllerV3) handler; + Map paramMap = createParamMap(request); + ModelView mv = controller.process(paramMap); + return mv; + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } +} +``` + +

+ +```java +public boolean supports(Object handler) { + return (handler instanceof ControllerV3); +} +``` +`ControllerV3`를 처리할 수 있는 어댑터. + + +```java +public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) { + ControllerV3 controller = (ControllerV3) handler; + Map paramMap = createParamMap(request); + ModelView mv = controller.process(paramMap); + return mv; +} +``` +handler를 컨트롤러 V3로 변환한 다음 V3 형식에 맞추도록 호출.
+`support()`를 통해 `ControllerV3`만 지원하기 때문에 타입 변환은 걱정없이 실행해도 됨.
+ControllerV3는 ModelView를 반환하므로 그대로 ModelView를 반환하면 됨. + + + +*FrontControllerServletV5* +```java +package hello.servlet.web.frontcontroller.v5; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3; +import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/ v5/*") +public class FrontControllerServletV5 extends HttpServlet { + private final Map handlerMappingMap = new HashMap<>(); + private final List handlerAdapters = new ArrayList<>(); + public FrontControllerServletV5() { + initHandlerMappingMap(); + initHandlerAdapters(); + } + private void initHandlerMappingMap() { + handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3()); + } + private void initHandlerAdapters() { + handlerAdapters.add(new ControllerV3HandlerAdapter()); + } + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + Object handler = getHandler(request); + if (handler == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + MyHandlerAdapter adapter = getHandlerAdapter(handler); + ModelView mv = adapter.handle(request, response, handler); + MyView view = viewResolver(mv.getViewName()); + view.render(mv.getModel(), request, response); + } + private Object getHandler(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return handlerMappingMap.get(requestURI); + } + private MyHandlerAdapter getHandlerAdapter(Object handler) { + for (MyHandlerAdapter adapter : handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } + } + throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler); + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} +``` + +**컨트롤러(Controller) -> 핸들러(Handler)**
+이전에는 컨트롤러를 직접 매핑해서 사용했다. 이젠느 어댑터를 사용하기 때문에, 컨트롤러 뿐 아니라 어댑터가 지원하기만 하면, +어떤 것이라도 URL에 매핑해 사용할 수 있다. 그래서 이름을 컨트롤러에서 더 넓은 범위의 핸들러로 변경했다. + + +**생성자** +```java + public FrontControllerServletV5() { + initHandlerMappingMap(); + initHandlerAdapters(); + } +``` +생성자는 핸들러 매핑과 어댑터를 초기화(등록)함 + + +**매핑 정보**
+`private final Map handlerMappingMap = new HashMap<>();`
+ +매핑 정보의 값이 ControllerV3 , ControllerV4 같은 인터페이스에서 아무 값이나 받을 수 있는 Object 로 변 +경되었다 + + +**핸들러 매핑**
+`Object handler = getHandler(request);` +```java + private Object getHandler(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return handlerMappingMap.get(requestURI); + } +``` +핸들러 매핑 정보인 `handlerMappingMap`에서 URL에 매핑된 핸들러(컨트롤러)객체를 찾아 반환 + +

+ +**핸들러를 처리할 수 있는 어댑터 조회**
+`MyHandlerAdapter adapter = getHandlerAdapter(handler)` +``` +for (MyHandlerAdapter adapter : handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } +} +``` + +`handler`를 처리활 수 있는 어댑터를 `adapter.supports(handler)`를 통해 찾음.
+handler가 `ControllerV3` 인터페이스를 구현했다면, `ControllerV3HandlerAdapter()`객체가 반환됨. + + +**어댑터 호출**
+`ModelView mv = adapter.handler(request, response, handler);` + +어댑터의 `handler(request, response, handler)`메서드를 통해 실제 어댑터가 호출됨.
+어댑터는 handler(컨트롤러)를 호출하고 그 결과를 어댑터에 맞추어 반환. +`ControllerV3HandlerAdapter`의 경우 어댑터의 모양과 컨트롤러의 모양이 유사해 변환 로직이 단순함 + + + +## 유연한 컨트롤러2 - v5 + +*FrontControllerServletV5* - `Controller4` 기능 추가 +```java +package hello.servlet.web.frontcontroller.v5; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3; +import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4; +import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter; +import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*") +public class FrontControllerServletV5 extends HttpServlet { + private final Map handlerMappingMap = new HashMap<>(); + private final List handlerAdapters = new ArrayList<>(); + public FrontControllerServletV5() { + initHandlerMappingMap(); + initHandlerAdapters(); + } + private void initHandlerMappingMap() { + handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3()); + + handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4()); + handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4()); + handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4()); + } + + private void initHandlerAdapters() { + handlerAdapters.add(new ControllerV3HandlerAdapter()); + handlerAdapters.add(new ControllerV4HandlerAdapter()); + } + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + Object handler = getHandler(request); + if (handler == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + MyHandlerAdapter adapter = getHandlerAdapter(handler); + ModelView mv = adapter.handle(request, response, handler); + MyView view = viewResolver(mv.getViewName()); + view.render(mv.getModel(), request, response); + } + private Object getHandler(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return handlerMappingMap.get(requestURI); + } + private MyHandlerAdapter getHandlerAdapter(Object handler) { + for (MyHandlerAdapter adapter : handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } + } + throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler); + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} +``` + +`handlerMappingMap`에 `ControllerV4`를 사용하는 컨트롤러를 추가, 해당 컨트롤러를 처리할 수 있는 어댑터인 `ControllerV4HandlerAdapter`도 추가 + + +*ControllerV4HandlerAdapter* +```java +package hello.servlet.web.frontcontroller.v5.adapter; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v4.ControllerV4; +import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class ControllerV4HandlerAdapter implements MyHandlerAdapter { + @Override + public boolean supports(Object handler) { + return (handler instanceof ControllerV4); + } + + @Override + public ModelView handle(HttpServletRequest request, HttpServletResponse + response, Object handler) { + ControllerV4 controller = (ControllerV4) handler; + Map paramMap = createParamMap(request); + Map model = new HashMap<>(); + String viewName = controller.process(paramMap, model); + + ModelView mv = new ModelView(viewName); + mv.setModel(model); + + return mv; + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } +} +``` + +분석 +```java + public boolean supports(Object handler) { + return (handler instanceof ControllerV4); + } +``` +`handler`가 `ControllerV4`인 경우에만 처리하는 어댑터. + +실행로직 +```java + ControllerV4 controller = (ControllerV4) handler; + Map paramMap = createParamMap(request); + Map model = new HashMap<>(); + String viewName = controller.process(paramMap, model); +``` +handler를 ControllerV4로 케스팅, paramMap, model을 만들어 해당 컨트롤러를 호출한다. +그리고 viewName을 반환 받는다. + + +**어댑터 반환** +``` + ModelView mv = new ModelView(viewName); + mv.setModel(model); + + return mv; +``` +어댑터에서 이 부분이 중요한 부분임 + +어댑터가 호출하는 `ControllerV4`는 뷰의 이름알 반환. 그런데 어댑터는 뷰의 이름이 아니라 `ModelView`를 만들어 반환해야함 +여기서 어댑터가 꼭 필요한 이유가 나옴.
+`ControllerV4`는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식을 맞춰 반환함. +마치 110v 콘센트를 220v 콘센트로 변경하듯. + + +**ControllerV4 & Adapter** +```java +public interface ControllerV4 { + String process(Map paramMap, Map model); +} +public interface MyHandlerAdapter { + ModelView handle(HttpServletRequest request, HttpServletResponse response, +Object handler) throws ServletException, IOException; +} +``` + + + + +--- \ No newline at end of file diff --git a/9 WEEK/servlet/SECTION5.md b/9 WEEK/servlet/SECTION5.md new file mode 100644 index 00000000..3d909129 --- /dev/null +++ b/9 WEEK/servlet/SECTION5.md @@ -0,0 +1,634 @@ +# 5. 스프링 MVC - 구조 이해 + +--- + +## 스프링 MVC 전체 구조 + +**직접 만든 MVC 프레임워크 구조** +![S5-1.png](img%2FS5-1.png) + +**Spring MVC 구조** +![S5-2.png](img%2FS5-2.png) + + +**직접 만든 프레임워크 -> 스프링 MVC 비교** +- FrontController -> DispatcherServlet +- handlerMappingMap -> HandlerMapping +- MyHandlerAdapter -> HandlerAdapter +- viewResolver -> ViewResolver +- MyView -> View + + +### DispatcherServlet 구조 살펴보기 + +`org.springframewokr.web.servlet.DispatcherServlet`
+ +스프링 MVC도 프론트 컨트롤러 패턴으로 구현됨.
+스프링 MVC의 프론트 컨트롤러가 바로 DispatcherServlet.
+그리고 이 디스패처 서빌릇이 바로 스프링 MVC의 핵심. + + +**DispatcherServlet 서블릿 등록** +- `DispatcherServlet`도 부모 클래스에서 `HttpServlet`을 상속 받아 사용하고, 서블릿으로 동작한다. + - DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet +- 스프링 부트는 `DispatcherServlet`을 서블릿으로 자동으로 등록하면서 *모든 경로*(`urlPatterns="/"`)에 대해 매핑한다. + - 참고 : 더 자세한 경로가 우선순위가 높음. 그래서 기존에 등록한 서블릿도 함께 동작 + +**요청 흐름** +- 서블릿이 호출되면 `HttpServlet`이 제공하는 `service()`가 호출된다. +- 스프링 MVC는 `DispatcherServlet`의 부모인 `FrameworkServlet`에서 `service()`를 오버라이드 해두었다. +- `FrameworkServlet.service()`를 시작으로 여러 메서드가 호출되면서 `DispatcherSerlvet.doDispatch()`가 호출된다. + + + + + +**Spring MVC 구조** +![S5-2.png](img%2FS5-2.png) + +*동작 순서* +1. **핸들러 조회** : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다. +2. **핸들러 어댑터 조회** : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다. +3. **핸들러 어댑터 실행** : 핸들러 어댑터를 실행한다. +4. **핸들러 실행** : 핸들러 어댑터가 실제 핸들러를 실행한다. +5. **ModelAndView 반환** : 핸들러 어댑터는 핸들러가 반환하는 정보를 `ModelAndView`로 **변환** +해서 반환한다. +6. **viewResolver 호출** : 뷰 리졸버를 찾고 실행한다. + - JSP의 경우 : `interfaceResourceViewResolver`가 자동 등록되고 사용된다. +7. **View반환** : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 런데링 역할을 담당하는 뷰 객체를 반환한다. + - JSP의 경우 : `interfaceResourceView(JstView)`를 반환하는데, 내부에 `forward()` 로직이 있다. +8. *viewRendering* : 뷰를 통해 뷰를 렌더링 한다. + + +**인터페이스 살펴보기** +- 스프링 MVC의 큰 강점은 `DispatcherServlet` 코드의 변경 없이, 원하는 기능을 변경하거나 확장할 수 있다는 점이다. +지금까지 설명한 대부분을 확장 가능할 수 있게 인터페이스로 제공한다. +- 이 인터페이스들만 구현해 `DispatcherServlet`에 등록하면 자신만의 컨트롤러를 만들 수 있다. + + + + + +**주요 인터페이스 목록** +- 핸들러 매핑: `org.springframework.web.servlet.HandlerMapping` +- 핸들러 어댑터: `org.springframework.web.servlet.HandlerAdapter` +- 뷰 리졸버: `org.springframework.web.servlet.ViewResolver` +- 뷰: `org.springframework.web.servlet.View` + + + +**정리**
+스프링 MVC는 코드 분량도 매우 많고, 복잡해서 내부 구조를 다 파악하는 것은 쉽지 않음. +사실 해당 기능을 직접 확장 하거나 나만의 컨트롤러를 만드는 일은 없으므로 걱정하지 않아도 됨. +왜냐하면 스프링 MVC는 전세계 수 많은 개발자들의 요구사항에 맞추어 기능을 계속 확장해왔고, +그래서 웹 애플리케이션을 만들 때 필요로 하는 대부분의 기능이 이미 다 구현되어 있음 +그래도 이렇게 핵심 동작방식을 알아두어야 향후 문제가 발생했을 때 어떤 부분에서 문제가 발생했는지 쉽게 파악하고, +문제를 해결할 수 있음. +그리고 확장 포인트가 필요할 때, 어떤 부분을 확장해야 할지 감을 잡을 수 있음. + + + + + +## 핸들러 매핑과 핸들러 어댑터 + +### Controller 인터페이스 + +**과거 버전 스프링 컨트롤러**
+`org.springframework.web.servlet.mvc.Controller` +```java +public interface Controller { + ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse + response) throws Exception; +} +``` + +> **참고**
+> `Controller`인터페이스는 `@Controller`애노테이션과 전혀 다르다. + + + +*OldController* +```java +package hello.servlet.web.springmvc.old; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +@Component("/springmvc/old-controller") +public class OldController implements Controller { + @Override + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + System.out.println("OldController.handleRequest"); + return null; + } +} +``` +- `@Component` : 이 컨트롤러는 `/springmvc/old-controller`라는 이름의 스프링 빈으로 등록되었다. +- **빈의 이름으로 URL을 매핑**할 것 + + +이 컨트롤러가 호출되려면 2가지가 필요하다. +- **HandlerMapping(핸들러 매핑)** + - 핸들러 매핑에사 이 컨트롤을 찾을 수 있어야 한다. + - 예) **스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑**이 필요하다. +- **HandlerAdapter(핸들러 어댑터)** + - 핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다. + - 예) `Controller` 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다. + +스프링은 이미 필요한 핸들러 매핑과 어댑터를 대부분 구현해 두었다.개발자가 직접 핸들러 매핑과 핸들러 어댑터를 만드는 일은 거의 없다. + + + +**스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터** +
+**HandlerMapping** +``` +0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용 +1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다. +``` + +**HandlerAdapter** +``` +0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용 +1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리 +2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리 +``` +핸들러 매핑, 핸들러 어댑터도 모두 순서대로 찾고, 없으면 다음 순서로 넘어감. + + +**1. 핸들러 매핑으로 핸들러 조회** +1. `HandlerMapping`을 순서대로 실행해 핸들러를 찾음 +2. 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는 +`BeanNameUrlHandlerMapping`가 실행에 성공하고 핸들러인 `OldController`를 반환. + + +**2. 핸들러 어댑터 조회** +1. `HandlerAdapter`의 `supports()`를 순서대로 호출. +2. `SimpleControllerHandlerAdapter`가 `Controller` 인터페이스를 지원하는 대상이 됨. + + +**3. 핸들러 어댑터 실행** +1. 디스패처 서블릿이 조회한 `SimpleControllerHandlerAdapter`를 실행하면서 핸들러 정보도 함께 넘겨줌 +2. `SimpleControllerHandlerAdapter` 는 핸들러인 `OldController` 를 내부에서 실행하고, +그 결과를 반환. + + +> 정리 - OldController 핸들러매핑, 어댑터
+> `OldController` 를 실행하면서 사용된 객체는 다음과 같다.
+> `HandlerMapping = BeanNameUrlHandlerMapping`
+> `HandlerAdapter = SimpleControllerHandlerAdapte` + + + +### HttpRequestHandler +`HttpRequestHandler`핸들러(컨트롤러)는 **서블릿과 가장 유사한 형태**의 핸들러. + +*MyHttpRequestHandler* +```java +package hello.servlet.web.springmvc.old; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.HttpRequestHandler; + +import java.io.IOException; + +@Component("/springmvc/request-handler") +public class MyHttpRequestHandler implements HttpRequestHandler { + @Override + public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("MyHttpRequestHandler.handleRequest"); + } +} +``` + +**1. 핸들러 매핑으로 핸들러 조회** +1. `HandlerMapping` 을 순서대로 실행해서, 핸들러를 찾는다. +2. 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는 +`BeanNameUrlHandlerMapping` 가 실행에 성공하고 핸들러인 `MyHttpRequestHandler` 를 반환한다. + +**2. 핸들러 어댑터 조회** +1. `HandlerAdapter` 의 `supports()` 를 순서대로 호출한다. +2. `HttpRequestHandlerAdapter` 가 `HttpRequestHandler` 인터페이스를 지원하므로 대상이 된다. + +**3. 핸들러 어댑터 실행** +1. 디스패처 서블릿이 조회한 HttpRequestHandlerAdapter 를 실행하면서 핸들러 정보도 함께 넘겨준다. +2. HttpRequestHandlerAdapter 는 핸들러인 MyHttpRequestHandler 를 내부에서 실행하고, +그 결과를 반환한다. + + +> **정리 - MyHttpRequestHandler 핸들러매핑, 어댑터** +> `MyHttpRequestHandler` 를 실행하면서 사용된 객체는 다음과 같다.
+> `HandlerMapping = BeanNameUrlHandlerMapping`
+> `HandlerAdapter = HttpRequestHandlerAdapter` + + +**@RequestMapping**
+가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는
+`RequestMappingHandlerMapping`,
+`RequestMappingHandlerAdapter` 이다.
+@RequestMapping 의 앞글자를 따서 만든 이름인데, 이것이 바로 지금 스프링에서 주로 사용하는 애노테이션 기반의 +컨트롤러를 지원하는 매핑과 어댑터이다. + + +## 뷰 리졸버 + +**OldController - View 조회할 수 있도록 변경** +```java +package hello.servlet.web.springmvc.old; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +@Component("/springmvc/old-controller") +public class OldController implements Controller { + @Override + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + System.out.println("OldController.handleRequest"); + return new ModelAndView("new-form"); + } +} +``` +View를 사용할 수 있도록 `return new ModelAndView("new-form");` 를 추가함.
+또한 `application.properties`에 다음 코드를 추가한다. +``` +spring.mvc.view.prefix=/WEB-INF/views/ +spring.mvc.view.suffix=.jsp +``` + +**뷰 리졸버 - InternalResourceViewResolver**
+스프링부트는 `InternalResourceViewResolver`라느 뷰 리졸버를 자동으로 등록, 이때 +`application.properties`에 등록한 `spring.mvc.view.prefix=/WEB-INF/views/`, +`spring.mvc.view.suffix=.jsp` 정보를 사용해 등록함. + +
아래의 방법도 가능하다. 다만 권장하지는 않는다. +``` +return new ModelAndView("/WEB-INF/views/new-form.jsp"); +``` + + +### 뷰 리졸버 동작 방식 + +**Spring MVC 구조** +![S5-2.png](img%2FS5-2.png) + +**스프링 부트가 자동 등록하는 뷰 리졸버**
+``` +1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능 +에 사용) +2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다. +``` + + +**1. 핸들러 어댑터 호출** +- 핸들러 어댑터를 통해 `new-form`이라는 논리 뷰 이름을 획득. + +**2. ViewResolver 호출** +- `new-form`이라는 뷰 이름으로 viewResolver를 순서대로 호출 +- `BeanNameViewResolver`는 `new-form`이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없음. +- `InternalResourceViewResolver`가 호출됨. + +**3. InternalResourceViewResolver** +- 이 뷰 리졸버는 `InternalResourceView`를 반환 + +**4. 뷰 - InternalResourceView** +- `InternalResourceView`는 JSP처럼 포워드`forward()`를 호출해 처리할 수 있는 경우에 사용 + +**5. view.render()** +- `view.render()`가 호출, `InternalResourceView`는 `forward()`를 사용해 JPS를 실행 + + +> **참고**
+> `InternalResourceViewResolver`는 만약 JSTL 라이브러리가 있으면 `InternalResourceView`를 상속받은 +> `JstlView`를 반환. `JstlView`는 JSTL태그 사용시 야간의 부가 기능이 추가됨. + +> **참고**
+> 다른 뷰는 실제 뷰를 렌더링하지만, JSP의 경우 `forward()` 통해서 해당 JSP로 이동(실행)해야 렌더링이 됨. +> JSP를 제외한 나머지 뷰 템플릿들은 `forward()`과정 없이 바로 렌더링 됨. + +> **참고**
+> Thymeleaf 뷰 템플릿을 사용하면 `ThymeleafViewResolver` 를 등록해야 함. +> 최근에는 라이브러리만 추가하면 스프링 부트가 이런 작업을 모두 자동화해줌. + + +## 스프링 MVC - 시작하기 + +**@RequestMapping**
+스프링은 애노테이션을 활용한 매우 유연하고, 실용적인 컨트롤러를 만들었는데 이것이 바로 +`@RequestMapping`애노테이션을 사용하는 컨트롤러임.
+ +`@RequestMapping` +- `RequestMappingHandlerMapping` +- `RequestMappingHandlerAdapter` + +가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 위와 같다.
+`@RequestMapping`의 앞글자를 따서 만든 이름인데, 이것이 바로 지금 스프링에서 주로 사용하는 +애노테이션 기반의 컨트롤러를 지원하는 핸들러 매핑과 어댑터이다. **실무에서는 99.9% 이 방식의 컨트롤러르 사용**한다. + + +지금까지 만들었던 프레임워크에서 사용했던 컨트롤러를 `@RequestMapping`기반의 스프링 MVC컨트롤러로 변경한다. + + + +*SpringMemberFormControllerV1 - 회원 등록 폼* +```java +package hello.servlet.web.springmvc.v1; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +@Controller +public class SpringMemverFormControllerV1 { + + @RequestMapping("/springmvc/v1/members/new-form") + public ModelAndView process() { + return new ModelAndView("new-form"); + } +} +``` +- `@Controller` + - 스프링이 자동으로 스프링 빈으로 등록. (내부에 `@Component` 애노테이션이 있어서 컴포넌트 스캔의 대상이 됨.) + - 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식. +- `@RequestMapping` : 요청 정보를 매핑. 해당 URL이 호출되면 이 메서드가 호출됨. +애노테이션을 기반으로 동작하기 때문에 메서드의 이름은 임의로 지으면 된다. +- `ModelAndView` : 모델과 뷰의 정보를 담아 반환하면 됨. + + + +`RequestMappingHandlerMapping`은 스프링 빈 중에서 `@RequestMapping` or `@Controller`가 클래스 +레벨에 붙어 있는 경우 매핑 정보로 인식.
+따라서 아래의 코드도 같은 동작을 수행함 +```java +@Component //컴포넌트 스캔을 통해 스프링 빈으로 등록 +@RequestMapping +public class SpringMemberFormControllerV1 { + @RequestMapping("/springmvc/v1/members/new-form") + public ModelAndView process() { + return new ModelAndView("new-form"); + } +} +``` +물론 `@Component`이 없어도 동작한다. + + + + +> **주의! - 스프링 3.0 이상
** +> 스프링 부트 3.0(스프링 프레임워크 6.0)부터는 클래스 레벨에 @RequestMapping 이 있어도 스프링 컨트롤러로 인 +> 식하지 않는다. 오직 @Controller 가 있어야 스프링 컨트롤러로 인식한다. 참고로 @RestController 는 해당 애 +> 노테이션 내부에 @Controller 를 포함하고 있으므로 인식 된다. 따라서 @Controller 가 없는 위의 두 코드는 스 +> 프링 컨트롤러로 인식되지 않는다. ( RequestMappingHandlerMapping 에서 @RequestMapping 는 +> 이제 인식하지 않고, Controller 만 인식한다.) + + +*SpringMemberSaveControllerV1 - 회원 저장* +```java +package hello.servlet.web.springmvc.v1; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import java.util.Map; + +@Controller +public class SpringMemberSaveControllerV1 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @RequestMapping("/springmvc/v1/members/save") + public ModelAndView process(HttpServletRequest request, HttpServletResponse response) { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + ModelAndView mv = new ModelAndView("save-result"); + mv.addObject("member", member); + return mv; + } +} +``` +- `mv.addObject("member", member)` + - 스프링이 제공하는 `ModelAndView`를 통해 Model 데이터를 추가할 때 `addObject()`를 사용하면 됨. + 이 데이터는 이후 뷰를 렌더링할 때 사용된다. + + +*SpringMemberListControllerV1 - 회원 목록* +```java +package hello.servlet.web.springmvc.v1; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import java.util.List; + +@Controller +public class SpringMemberListControllerV1 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @RequestMapping("/springmvc/v1/members") + public ModelAndView process() { + List members = memberRepository.findAll(); + ModelAndView mv = new ModelAndView("members"); + mv.addObject("members", members); + + return mv; + + } +} +``` + + + +## 스프링 MVC - 컨트롤러 통합 +`@RequestMapping`을 잘 보면 클래스 단위가 아니라 메서드 단위에 적용된 것을 확인할 수 있다. +따라서 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다. + + +*SpringMemberControllerV2* +```java +package hello.servlet.web.springmvc.v2; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import java.util.List; + +@Controller +@RequestMapping("/springmvc/v2/members") +public class SpringMemberControllerV2 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @RequestMapping("/new-form") + public ModelAndView newForm() { + return new ModelAndView("new-form"); + } + + + @RequestMapping() + public ModelAndView save() { + List members = memberRepository.findAll(); + ModelAndView mv = new ModelAndView("members"); + mv.addObject("members", members); + + return mv; + + } + + + @RequestMapping("/save") + public ModelAndView members(HttpServletRequest request, HttpServletResponse response) { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + ModelAndView mv = new ModelAndView("save-result"); + mv.addObject("member", member); + return mv; + } +} +``` + +**조합** +컨트롤러 클래스를 통합하는 것을 넘어 조합도 가능.
+`/springmvc/v2/members`가 중복이 있었다. +- `@RequestMapping("/springmvc/v2/members/new-form")` +- `@RequestMapping("/springmvc/v2/members")` +- `@RequestMapping("/springmvc/v2/members/save")` + +클래스 레벨에서 `@RequestMapping`을 두면 메서드 레벨과 조합된다. +```java +@Controller +@RequestMapping("/springmvc/v2/members") +public class SpringMemberControllerV2{} +``` +**조합 결과** +- `@RequestMapping("/springmvc/v2/members/new-form")` -> +- `@RequestMapping("/springmvc/v2/members")` -> +- `@RequestMapping("/springmvc/v2/members/save")` -> + +| 조합 전 | 조합 후 | +|------|--------------------------------| +| `@RequestMapping("/springmvc/v2/members/new-form")` | `@RequestMapping("/new-form")` | +| `@RequestMapping("/springmvc/v2/members")` | `@RequestMapping("")` | +| `@RequestMapping("/springmvc/v2/members/save")` | `@RequestMapping("/save")` | + + + + +## 스프링 MVC - 실용적인 방법 +MVC 프레임워크 만들기에서 V3는 ModelView를 개발자가 직접 생성해 반환했기 떄문에 불편했다. + +**다음은 실무에서 사용하는 방식이다.** + +*SpringMemberControllerV3* +```java +package hello.servlet.web.springmvc.v3; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@Controller +@RequestMapping("/springmvc/v3/members") +public class SpringMemberControllerV3 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @GetMapping("/new-form") + public String newForm() { + return "new-form"; + } + + + @PostMapping("/save") + public String save(@RequestParam("username") String username, @RequestParam("age") int age, Model model) { + Member member = new Member(username, age); + memberRepository.save(member); + model.addAttribute("member", member); + return "save-result"; + } + + + @GetMapping("") + public String members(Model model) { + List members = memberRepository.findAll(); + model.addAttribute("members", members); + return "members"; + } + + +} +``` + +**Model 파라미터**
+`save()`, `members()`를 보면 Model을 파라미터로 받는 것을 확인할 수 있다. 스프링 MVC도 이런 편의 기능을 제공한다. + + +**ViewName 직접 반환**
+뷰의 논리 이름을 반환한다. + + +**@RequestParam**
+스프링은 HTTP 요청 파라미터를 `@RequsetParam`으로 받을 수 있다. +`@RequestParam("username")`은 `request.getParameter("username")`과 거의 같은 코드이다. +
GET쿼리 파라미터, POST Form 방식을 모두 지원한다. + + +**@RequestMapping @GetMapping, @PostMapping**
+`@RequestMapping` 은 URL만 매칭하는 것이 아니라, HTTP Method도 함께 구분할 수 있다.
+예를 들어서 URL이 `/new-form` 이고, HTTP Method가 GET인 경우를 모두 만족하는 매핑을 하려면 다음과 같이 +처리하면 된다. +``` +@RequestMapping(value = "/new-form", method = RequestMethod.GET) +``` +이것을 @GetMapping , @PostMapping 으로 더 편리하게 사용할 수 있다.
+참고로 Get, Post, Put, Delete, Patch 모두 애노테이션이 준비되어 있다.
+ + + + + + + + + + + + +--- \ No newline at end of file diff --git a/9 WEEK/servlet/build.gradle b/9 WEEK/servlet/build.gradle new file mode 100644 index 00000000..de959ed2 --- /dev/null +++ b/9 WEEK/servlet/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'java' + id 'war' + id 'org.springframework.boot' version '3.2.1' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'hello' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + + //JSP 추가 시작 + implementation 'org.apache.tomcat.embed:tomcat-embed-jasper' + implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상 + implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //스프링부트3.0 이상 + implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상 + //JSP 추가 끝 + + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar b/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.jar differ diff --git a/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties b/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1af9e093 --- /dev/null +++ b/9 WEEK/servlet/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/9 WEEK/servlet/gradlew b/9 WEEK/servlet/gradlew new file mode 100644 index 00000000..1aa94a42 --- /dev/null +++ b/9 WEEK/servlet/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/9 WEEK/servlet/gradlew.bat b/9 WEEK/servlet/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/9 WEEK/servlet/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/9 WEEK/servlet/img/S1-1.png b/9 WEEK/servlet/img/S1-1.png new file mode 100644 index 00000000..b4382b9b Binary files /dev/null and b/9 WEEK/servlet/img/S1-1.png differ diff --git a/9 WEEK/servlet/img/S1-2.png b/9 WEEK/servlet/img/S1-2.png new file mode 100644 index 00000000..ff31b6a6 Binary files /dev/null and b/9 WEEK/servlet/img/S1-2.png differ diff --git a/9 WEEK/servlet/img/S1-3.png b/9 WEEK/servlet/img/S1-3.png new file mode 100644 index 00000000..d0a945c4 Binary files /dev/null and b/9 WEEK/servlet/img/S1-3.png differ diff --git a/9 WEEK/servlet/img/S1-4.png b/9 WEEK/servlet/img/S1-4.png new file mode 100644 index 00000000..b2538fe4 Binary files /dev/null and b/9 WEEK/servlet/img/S1-4.png differ diff --git a/9 WEEK/servlet/img/S1-5.png b/9 WEEK/servlet/img/S1-5.png new file mode 100644 index 00000000..496dd994 Binary files /dev/null and b/9 WEEK/servlet/img/S1-5.png differ diff --git a/9 WEEK/servlet/img/S1-6.png b/9 WEEK/servlet/img/S1-6.png new file mode 100644 index 00000000..0f147271 Binary files /dev/null and b/9 WEEK/servlet/img/S1-6.png differ diff --git a/9 WEEK/servlet/img/S1-7.png b/9 WEEK/servlet/img/S1-7.png new file mode 100644 index 00000000..cc6ffda9 Binary files /dev/null and b/9 WEEK/servlet/img/S1-7.png differ diff --git a/9 WEEK/servlet/img/S1-8.png b/9 WEEK/servlet/img/S1-8.png new file mode 100644 index 00000000..dafb1dc7 Binary files /dev/null and b/9 WEEK/servlet/img/S1-8.png differ diff --git a/9 WEEK/servlet/img/S2-1.png b/9 WEEK/servlet/img/S2-1.png new file mode 100644 index 00000000..2f468be2 Binary files /dev/null and b/9 WEEK/servlet/img/S2-1.png differ diff --git a/9 WEEK/servlet/img/S2-2.png b/9 WEEK/servlet/img/S2-2.png new file mode 100644 index 00000000..c2d94f77 Binary files /dev/null and b/9 WEEK/servlet/img/S2-2.png differ diff --git a/9 WEEK/servlet/img/S2-3.png b/9 WEEK/servlet/img/S2-3.png new file mode 100644 index 00000000..15d29abf Binary files /dev/null and b/9 WEEK/servlet/img/S2-3.png differ diff --git a/9 WEEK/servlet/img/S2-4.png b/9 WEEK/servlet/img/S2-4.png new file mode 100644 index 00000000..a6ce3e89 Binary files /dev/null and b/9 WEEK/servlet/img/S2-4.png differ diff --git a/9 WEEK/servlet/img/S2-5.png b/9 WEEK/servlet/img/S2-5.png new file mode 100644 index 00000000..5c762ffa Binary files /dev/null and b/9 WEEK/servlet/img/S2-5.png differ diff --git a/9 WEEK/servlet/img/S2-6.png b/9 WEEK/servlet/img/S2-6.png new file mode 100644 index 00000000..c5924ca5 Binary files /dev/null and b/9 WEEK/servlet/img/S2-6.png differ diff --git a/9 WEEK/servlet/img/S2-7.png b/9 WEEK/servlet/img/S2-7.png new file mode 100644 index 00000000..3948838e Binary files /dev/null and b/9 WEEK/servlet/img/S2-7.png differ diff --git a/9 WEEK/servlet/img/S2-8.png b/9 WEEK/servlet/img/S2-8.png new file mode 100644 index 00000000..76f40bbd Binary files /dev/null and b/9 WEEK/servlet/img/S2-8.png differ diff --git a/9 WEEK/servlet/img/S4-1.png b/9 WEEK/servlet/img/S4-1.png new file mode 100644 index 00000000..3944b4aa Binary files /dev/null and b/9 WEEK/servlet/img/S4-1.png differ diff --git a/9 WEEK/servlet/img/S4-2.png b/9 WEEK/servlet/img/S4-2.png new file mode 100644 index 00000000..6f55a519 Binary files /dev/null and b/9 WEEK/servlet/img/S4-2.png differ diff --git a/9 WEEK/servlet/img/S4-3.png b/9 WEEK/servlet/img/S4-3.png new file mode 100644 index 00000000..d907f0d0 Binary files /dev/null and b/9 WEEK/servlet/img/S4-3.png differ diff --git a/9 WEEK/servlet/img/S4-4.png b/9 WEEK/servlet/img/S4-4.png new file mode 100644 index 00000000..bd12948f Binary files /dev/null and b/9 WEEK/servlet/img/S4-4.png differ diff --git a/9 WEEK/servlet/img/S4-5.png b/9 WEEK/servlet/img/S4-5.png new file mode 100644 index 00000000..beb9c402 Binary files /dev/null and b/9 WEEK/servlet/img/S4-5.png differ diff --git a/9 WEEK/servlet/img/S4-6.png b/9 WEEK/servlet/img/S4-6.png new file mode 100644 index 00000000..f2539420 Binary files /dev/null and b/9 WEEK/servlet/img/S4-6.png differ diff --git a/9 WEEK/servlet/img/S4-7.png b/9 WEEK/servlet/img/S4-7.png new file mode 100644 index 00000000..c805b5b0 Binary files /dev/null and b/9 WEEK/servlet/img/S4-7.png differ diff --git a/9 WEEK/servlet/img/S5-1.png b/9 WEEK/servlet/img/S5-1.png new file mode 100644 index 00000000..b560216c Binary files /dev/null and b/9 WEEK/servlet/img/S5-1.png differ diff --git a/9 WEEK/servlet/img/S5-2.png b/9 WEEK/servlet/img/S5-2.png new file mode 100644 index 00000000..5f9beda2 Binary files /dev/null and b/9 WEEK/servlet/img/S5-2.png differ diff --git a/9 WEEK/servlet/settings.gradle b/9 WEEK/servlet/settings.gradle new file mode 100644 index 00000000..9c5e4d1a --- /dev/null +++ b/9 WEEK/servlet/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'servlet' diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java b/9 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java new file mode 100644 index 00000000..a1c772de --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/ServletApplication.java @@ -0,0 +1,15 @@ +package hello.servlet; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.ServletComponentScan; + +@ServletComponentScan //서블릿 자동 등록 +@SpringBootApplication +public class ServletApplication { + + public static void main(String[] args) { + SpringApplication.run(ServletApplication.class, args); + } + +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java b/9 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java new file mode 100644 index 00000000..057e157f --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/ServletInitializer.java @@ -0,0 +1,13 @@ +package hello.servlet; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +public class ServletInitializer extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(ServletApplication.class); + } + +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java new file mode 100644 index 00000000..c514bc33 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloData.java @@ -0,0 +1,11 @@ +package hello.servlet.basic; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class HelloData { + private String username; + private int age; +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java new file mode 100644 index 00000000..4a128f82 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/HelloServlet.java @@ -0,0 +1,26 @@ +package hello.servlet.basic; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "helloServlet", urlPatterns = "/hello") +public class HelloServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException { + System.out.println("HelloServlet.service"); + System.out.println("requset = " + requset); + System.out.println("response = " + response); + + String username = requset.getParameter("username"); + System.out.println("username = " + username); + + response.setContentType("text/plain"); + response.setCharacterEncoding("utf-8"); + response.getWriter().write("hello " + username); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java new file mode 100644 index 00000000..3bb95632 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyJsonServlet.java @@ -0,0 +1,35 @@ +package hello.servlet.basic.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import hello.servlet.basic.HelloData; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json") +public class RequestBodyJsonServlet extends HttpServlet { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + + System.out.println("messageBody = " + messageBody); + + HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); + + System.out.println("helloData.username = " + helloData.getUsername()); + System.out.println("helloData.age = " + helloData.getAge()); + + response.getWriter().write("ok"); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java new file mode 100644 index 00000000..660342ba --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestBodyStringServlet.java @@ -0,0 +1,25 @@ +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@WebServlet(name="requestBodyStringServlet", urlPatterns = "/request-body-string") +public class RequestBodyStringServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + + System.out.println("messageBody = " + messageBody); + + response.getWriter().write("OK"); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java new file mode 100644 index 00000000..3d19efca --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestHeaderServlet.java @@ -0,0 +1,108 @@ +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header") +public class RequestHeaderServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + printStartLine(request); + printHeaders(request); + printHeaderUtils(request); + printEtc(request); + + } + + //start line 정보 + private void printStartLine(HttpServletRequest request) { + System.out.println("--- REQUEST-LINE - start ---"); + System.out.println("request.getMethod() = " + request.getMethod()); //GET + System.out.println("request.getProtocol() = " + request.getProtocol()); //HTTP/1.1 + System.out.println("request.getScheme() = " + request.getScheme()); //http + // http://localhost:8080/request-header + System.out.println("request.getRequestURL() = " + request.getRequestURL()); + // /request-header + System.out.println("request.getRequestURI() = " + request.getRequestURI()); + //username=hi + System.out.println("request.getQueryString() = " + + request.getQueryString()); + System.out.println("request.isSecure() = " + request.isSecure()); //https 사용유무 + System.out.println("--- REQUEST-LINE - end ---"); + System.out.println(); + } + + //Header 모든 정보 + private void printHeaders(HttpServletRequest request) { + System.out.println("--- Headers - start ---"); + /* + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + System.out.println(headerName + ": " + request.getHeader(headerName)); + } + */ + request.getHeaderNames().asIterator() + .forEachRemaining(headerName -> System.out.println(headerName + ": " + + request.getHeader(headerName))); + System.out.println("--- Headers - end ---"); + System.out.println(); + } + + + //Header 편리한 조회 + private void printHeaderUtils(HttpServletRequest request) { + System.out.println("--- Header 편의 조회 start ---"); + System.out.println("[Host 편의 조회]"); + System.out.println("request.getServerName() = " + + request.getServerName()); //Host 헤더 + System.out.println("request.getServerPort() = " + + request.getServerPort()); //Host 헤더 + System.out.println(); + System.out.println("[Accept-Language 편의 조회]"); + request.getLocales().asIterator() + .forEachRemaining(locale -> System.out.println("locale = " + + locale)); + System.out.println("request.getLocale() = " + request.getLocale()); + System.out.println(); + System.out.println("[cookie 편의 조회]"); + if (request.getCookies() != null) { + for (Cookie cookie : request.getCookies()) { + System.out.println(cookie.getName() + ": " + cookie.getValue()); + } + } + System.out.println(); + System.out.println("[Content 편의 조회]"); + System.out.println("request.getContentType() = " + + request.getContentType()); + System.out.println("request.getContentLength() = " + + request.getContentLength()); + System.out.println("request.getCharacterEncoding() = " + + request.getCharacterEncoding()); + } + + //기타 정보 + private void printEtc(HttpServletRequest request) { + System.out.println("--- 기타 조회 start ---"); + System.out.println("[Remote 정보]"); + System.out.println("request.getRemoteHost() = " + + request.getRemoteHost()); // + System.out.println("request.getRemoteAddr() = " + + request.getRemoteAddr()); // + System.out.println("request.getRemotePort() = " + + request.getRemotePort()); // + System.out.println(); + System.out.println("[Local 정보]"); + System.out.println("request.getLocalName() = " + request.getLocalName()); // + System.out.println("request.getLocalAddr() = " + request.getLocalAddr()); // + System.out.println("request.getLocalPort() = " + request.getLocalPort()); // + System.out.println("--- 기타 조회 end ---"); + System.out.println(); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java new file mode 100644 index 00000000..d90e08d1 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/request/RequestParamServlet.java @@ -0,0 +1,39 @@ +package hello.servlet.basic.request; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Enumeration; + +@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param") +public class RequestParamServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest requset, HttpServletResponse response) throws ServletException, IOException { + System.out.println("[전체 파라미터 조회] - start"); + +// Enumeration parameterNames = requset.getParameterNames(); + requset.getParameterNames().asIterator().forEachRemaining( + paramName -> System.out.println(paramName + "=" + requset.getParameter(paramName)) + ); + + System.out.println("[전체 파라미터 조회] - end"); + System.out.println(); + + System.out.println("[단일 파라미터 조회]"); + String username= requset.getParameter("username"); + String age = requset.getParameter("age"); + + System.out.println("username = " + username); + System.out.println("age = " + age); + + System.out.println("[이름이 같은 복수 파라미터 조회]"); + String[] usernames = requset.getParameterValues("username"); + for(String name: usernames) { + System.out.println("username = " + name); + } + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java new file mode 100644 index 00000000..12923c0a --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHeaderServlet.java @@ -0,0 +1,62 @@ +package hello.servlet.basic.response; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header") +public class ResponseHeaderServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + //[status-line] + response.setStatus(HttpServletResponse.SC_OK); + + //[response-headers] + response.setHeader("Content-Type", "text/plain;charset=utf-8"); + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("my-header","hello"); + + //[Header 편의 메서드] +// content(response); +// cookie(response); +// redirect(response); + + //[message body] + PrintWriter writer = response.getWriter(); + writer.println("ok"); + } + + private void content(HttpServletResponse response) { + //Content-Type: text/plain;charset=utf-8 + //Content-Length: 2 + //response.setHeader("Content-Type", "text/plain;charset=utf-8"); + response.setContentType("text/plain"); + response.setCharacterEncoding("utf-8"); + //response.setContentLength(2); //(생략시 자동 생성) + } + + private void cookie(HttpServletResponse response) { + //Set-Cookie: myCookie=good; Max-Age=600; + //response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600"); + Cookie cookie = new Cookie("myCookie", "good"); + cookie.setMaxAge(600); //600초 + response.addCookie(cookie); + } + + private void redirect(HttpServletResponse response) throws IOException { + //Status Code 302 + //Location: /basic/hello-form.html + //response.setStatus(HttpServletResponse.SC_FOUND); //302 + //response.setHeader("Location", "/basic/hello-form.html"); + response.sendRedirect("/basic/hello-form.html"); + } + +} + diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java new file mode 100644 index 00000000..a64c8450 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseHtmlServlet.java @@ -0,0 +1,27 @@ +package hello.servlet.basic.response; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html") +public class ResponseHtmlServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + //Content-Type: text/html;charset=utf-8 + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + PrintWriter writer = response.getWriter(); + writer.println(""); + writer.println(""); + writer.println("
HI?
"); + writer.println(""); + writer.println(""); + } +} + diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java new file mode 100644 index 00000000..5260635b --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/basic/response/ResponseJsonServlet.java @@ -0,0 +1,30 @@ +package hello.servlet.basic.response; + +import com.fasterxml.jackson.databind.ObjectMapper; +import hello.servlet.basic.HelloData; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json") +public class ResponseJsonServlet extends HttpServlet { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + + HelloData helloData = new HelloData(); + helloData.setUsername("lee"); + helloData.setAge(20); + + String result = objectMapper.writeValueAsString(helloData); + response.getWriter().write(result); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java b/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java new file mode 100644 index 00000000..6fe75506 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/Member.java @@ -0,0 +1,18 @@ +package hello.servlet.domain.member; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Member { + + private Long id; + private String username; + private int age; + + public Member(String username, int age) { + this.username = username; + this.age = age; + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java b/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java new file mode 100644 index 00000000..15eaaf81 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/domain/member/MemberRepository.java @@ -0,0 +1,38 @@ +package hello.servlet.domain.member; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MemberRepository { + + private static Map store = new HashMap<>(); + private static long sequence = 0L; + + //싱글톤 + private static final MemberRepository instance = new MemberRepository(); + + public static MemberRepository getInstance() { + return instance; + } + private MemberRepository(){} + + public Member save(Member member) { + member.setId(++sequence); + store.put(member.getId(), member); + return member; + } + + public Member findById(Long id) { + return store.get(id); + } + + public List findAll() { + return new ArrayList<>(store.values()); + } + + public void clearStore() { + store.clear(); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java new file mode 100644 index 00000000..a6a33309 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/ModelView.java @@ -0,0 +1,23 @@ +package hello.servlet.web.frontcontroller; + +import java.util.HashMap; +import java.util.Map; +public class ModelView { + private String viewName; + private Map model = new HashMap<>(); + public ModelView(String viewName) { + this.viewName = viewName; + } + public String getViewName() { + return viewName; + } + public void setViewName(String viewName) { + this.viewName = viewName; + } + public Map getModel() { + return model; + } + public void setModel(Map model) { + this.model = model; + } +} \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java new file mode 100644 index 00000000..4da0c980 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/MyView.java @@ -0,0 +1,32 @@ +package hello.servlet.web.frontcontroller; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Map; + +public class MyView { + private String viewPath; + + public MyView(String viewPath) { + this.viewPath = viewPath; + } + public void render(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } + public void render(Map model, HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + modelToRequestAttribute(model, request); + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } + private void modelToRequestAttribute(Map model, + HttpServletRequest request) { + model.forEach((key, value) -> request.setAttribute(key, value)); + } +} \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java new file mode 100644 index 00000000..0e2f00a3 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1.java @@ -0,0 +1,11 @@ +package hello.servlet.web.frontcontroller.v1; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface ControllerV1 { + void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java new file mode 100644 index 00000000..6dc0cab1 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServiceV1.java @@ -0,0 +1,40 @@ +package hello.servlet.web.frontcontroller.v1; + +import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1; +import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1; +import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*") +public class FrontControllerServiceV1 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV1() { + controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1()); + controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1()); + controllerMap.put("/front-controller/v1/members", new MemberListControllerV1()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV1 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + controller.process(request, response); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java new file mode 100644 index 00000000..e4927c67 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.java @@ -0,0 +1,18 @@ +package hello.servlet.web.frontcontroller.v1.controller; + +import hello.servlet.web.frontcontroller.v1.ControllerV1; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberFormControllerV1 implements ControllerV1 { + @Override + public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String viewPath = "/WEB-INF/views/new-form.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java new file mode 100644 index 00000000..da67d7b2 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.java @@ -0,0 +1,28 @@ +package hello.servlet.web.frontcontroller.v1.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.v1.ControllerV1; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +public class MemberListControllerV1 implements ControllerV1 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + + request.setAttribute("members", members); + + String viewPath = "/WEB-INF/views/members.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java new file mode 100644 index 00000000..c089740b --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.java @@ -0,0 +1,30 @@ +package hello.servlet.web.frontcontroller.v1.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.v1.ControllerV1; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberSaveControllerV1 implements ControllerV1 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + request.setAttribute("member", member); + + String viewPath = "/WEB-INF/views/save-result.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java new file mode 100644 index 00000000..6e1de8a2 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2.java @@ -0,0 +1,12 @@ +package hello.servlet.web.frontcontroller.v2; + +import hello.servlet.web.frontcontroller.MyView; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface ControllerV2 { + MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java new file mode 100644 index 00000000..8a46b0c9 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServiceV2.java @@ -0,0 +1,42 @@ +package hello.servlet.web.frontcontroller.v2; + +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2; +import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*") +public class FrontControllerServiceV2 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV2() { + controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2()); + controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2()); + controllerMap.put("/front-controller/v2/members", new MemberListControllerV2()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV2 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + MyView view = controller.process(request, response); + view.render(request, response); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java new file mode 100644 index 00000000..ffdece7a --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2.java @@ -0,0 +1,16 @@ +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberFormControllerV2 implements ControllerV2 { + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + return new MyView("/WEB-INF/views/new-form.jsp"); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java new file mode 100644 index 00000000..1a69f49e --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberListControllerV2.java @@ -0,0 +1,24 @@ +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +public class MemberListControllerV2 implements ControllerV2 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + request.setAttribute("members", members); + return new MyView("/WEB-INF/views/members.jsp"); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java new file mode 100644 index 00000000..e56b5984 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberSaveControllerV2.java @@ -0,0 +1,28 @@ +package hello.servlet.web.frontcontroller.v2.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v2.ControllerV2; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class MemberSaveControllerV2 implements ControllerV2 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + request.setAttribute("member", member); + return new MyView("/WEB-INF/views/save-result.jsp"); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java new file mode 100644 index 00000000..ba2edc59 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3.java @@ -0,0 +1,9 @@ +package hello.servlet.web.frontcontroller.v3; + +import hello.servlet.web.frontcontroller.ModelView; + +import java.util.Map; + +public interface ControllerV3 { + ModelView process(Map paramMap); +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java new file mode 100644 index 00000000..46e92e6d --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServiceV3.java @@ -0,0 +1,56 @@ +package hello.servlet.web.frontcontroller.v3; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*") +public class FrontControllerServiceV3 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV3() { + controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3()); + controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3()); + controllerMap.put("/front-controller/v3/members", new MemberListControllerV3()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV3 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + Map paramMap = createParamMap(request); + ModelView mv = controller.process(paramMap); + String viewName = mv.getViewName(); + MyView view = viewResolver(viewName); + view.render(mv.getModel(), request, response); + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java new file mode 100644 index 00000000..4b886d83 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.java @@ -0,0 +1,13 @@ +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.Map; + +public class MemberFormControllerV3 implements ControllerV3 { + @Override + public ModelView process(Map paramMap) { + return new ModelView("new-form"); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java new file mode 100644 index 00000000..897cb7fd --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberListControllerV3.java @@ -0,0 +1,24 @@ +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.List; +import java.util.Map; + +public class MemberListControllerV3 implements ControllerV3 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public ModelView process(Map paramMap) { + List members = memberRepository.findAll(); + ModelView mv = new ModelView("members"); + mv.getModel().put("members", members); + + return mv; + + } + +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java new file mode 100644 index 00000000..cae1ba01 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3.java @@ -0,0 +1,25 @@ +package hello.servlet.web.frontcontroller.v3.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; + +import java.util.Map; + +public class MemberSaveControllerV3 implements ControllerV3 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public ModelView process(Map paramMap) { + String username = paramMap.get("username"); + int age = Integer.parseInt(paramMap.get("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + ModelView mv = new ModelView("save-result"); + mv.getModel().put("member", member); + return mv; + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java new file mode 100644 index 00000000..6a4a5803 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4.java @@ -0,0 +1,7 @@ +package hello.servlet.web.frontcontroller.v4; + +import java.util.Map; + +public interface ControllerV4 { + String process(Map paramMap, Map model); +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java new file mode 100644 index 00000000..065a4e8f --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServiceV4.java @@ -0,0 +1,58 @@ +package hello.servlet.web.frontcontroller.v4; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*") +public class FrontControllerServiceV4 extends HttpServlet { + private Map controllerMap = new HashMap<>(); + + public FrontControllerServiceV4() { + controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4()); + controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4()); + controllerMap.put("/front-controller/v4/members", new MemberListControllerV4()); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("FrontControllerServiceV1.service"); + + String requestURI = request.getRequestURI(); + + ControllerV4 controller = controllerMap.get(requestURI); + if(controller == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + Map paramMap = createParamMap(request); + Map model = new HashMap<>(); + + String viewName = controller.process(paramMap, model); + + MyView view = viewResolver(viewName); + view.render(model, request, response); + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberFormControllerV4.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberFormControllerV4.java new file mode 100644 index 00000000..288bdff0 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberFormControllerV4.java @@ -0,0 +1,12 @@ +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.Map; + +public class MemberFormControllerV4 implements ControllerV4 { + @Override + public String process(Map paramMap, Map model) { + return "new-form"; + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberListControllerV4.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberListControllerV4.java new file mode 100644 index 00000000..e5a43cfc --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberListControllerV4.java @@ -0,0 +1,22 @@ +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.List; +import java.util.Map; + +public class MemberListControllerV4 implements ControllerV4 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + public String process(Map paramMap, Map model) { + List members = memberRepository.findAll(); + + model.put("members", members); + return "members"; + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerV4.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerV4.java new file mode 100644 index 00000000..2a4c1128 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerV4.java @@ -0,0 +1,24 @@ +package hello.servlet.web.frontcontroller.v4.controller; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import hello.servlet.web.frontcontroller.v4.ControllerV4; + +import java.util.Map; + +public class MemberSaveControllerV4 implements ControllerV4 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + public String process(Map paramMap, Map model) { + String username = paramMap.get("username"); + int age = Integer.parseInt(paramMap.get("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + model.put("member", member); + + return "save-result"; + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5.java new file mode 100644 index 00000000..778e60db --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5.java @@ -0,0 +1,71 @@ +package hello.servlet.web.frontcontroller.v5; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.MyView; +import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3; +import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3; +import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4; +import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4; +import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter; +import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*") +public class FrontControllerServletV5 extends HttpServlet { + private final Map handlerMappingMap = new HashMap<>(); + private final List handlerAdapters = new ArrayList<>(); + public FrontControllerServletV5() { + initHandlerMappingMap(); + initHandlerAdapters(); + } + private void initHandlerMappingMap() { + handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3()); + handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3()); + + handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4()); + handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4()); + handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4()); + } + + private void initHandlerAdapters() { + handlerAdapters.add(new ControllerV3HandlerAdapter()); + handlerAdapters.add(new ControllerV4HandlerAdapter()); + } + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + Object handler = getHandler(request); + if (handler == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + MyHandlerAdapter adapter = getHandlerAdapter(handler); + ModelView mv = adapter.handle(request, response, handler); + MyView view = viewResolver(mv.getViewName()); + view.render(mv.getModel(), request, response); + } + private Object getHandler(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return handlerMappingMap.get(requestURI); + } + private MyHandlerAdapter getHandlerAdapter(Object handler) { + for (MyHandlerAdapter adapter : handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } + } + throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler); + } + private MyView viewResolver(String viewName) { + return new MyView("/WEB-INF/views/" + viewName + ".jsp"); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/MyHandlerAdapter.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/MyHandlerAdapter.java new file mode 100644 index 00000000..db175c53 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/MyHandlerAdapter.java @@ -0,0 +1,14 @@ +package hello.servlet.web.frontcontroller.v5; + +import hello.servlet.web.frontcontroller.ModelView; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface MyHandlerAdapter { + boolean supports(Object handler); + + ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException; +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter.java new file mode 100644 index 00000000..5453bbfa --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter.java @@ -0,0 +1,29 @@ +package hello.servlet.web.frontcontroller.v5.adapter; +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v3.ControllerV3; +import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; +public class ControllerV3HandlerAdapter implements MyHandlerAdapter { + @Override + public boolean supports(Object handler) { + return (handler instanceof ControllerV3); + } + @Override + public ModelView handle(HttpServletRequest request, HttpServletResponse + response, Object handler) { + ControllerV3 controller = (ControllerV3) handler; + Map paramMap = createParamMap(request); + ModelView mv = controller.process(paramMap); + return mv; + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } +} \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter.java new file mode 100644 index 00000000..99b967a9 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter.java @@ -0,0 +1,40 @@ +package hello.servlet.web.frontcontroller.v5.adapter; + +import hello.servlet.web.frontcontroller.ModelView; +import hello.servlet.web.frontcontroller.v4.ControllerV4; +import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class ControllerV4HandlerAdapter implements MyHandlerAdapter { + @Override + public boolean supports(Object handler) { + return (handler instanceof ControllerV4); + } + + @Override + public ModelView handle(HttpServletRequest request, HttpServletResponse + response, Object handler) { + ControllerV4 controller = (ControllerV4) handler; + Map paramMap = createParamMap(request); + Map model = new HashMap<>(); + String viewName = controller.process(paramMap, model); + + ModelView mv = new ModelView(viewName); + mv.setModel(model); + + return mv; + } + private Map createParamMap(HttpServletRequest request) { + Map paramMap = new HashMap<>(); + request.getParameterNames().asIterator() + .forEachRemaining(paramName -> paramMap.put(paramName, + request.getParameter(paramName))); + return paramMap; + } +} \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberFormServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberFormServlet.java new file mode 100644 index 00000000..8c127920 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberFormServlet.java @@ -0,0 +1,38 @@ +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form") +public class MemberFormServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + + PrintWriter w = response.getWriter(); + + w.write("\n" + + "\n" + + "\n" + + " \n" + + " Title\n" + + "\n" + + "\n" + + "
\n" + + " username: \n" + + " age: \n" + + " \n" + + "
\n" + + "\n" + + "\n"); } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberListServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberListServlet.java new file mode 100644 index 00000000..5a3dd1e6 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberListServlet.java @@ -0,0 +1,62 @@ +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members") +public class MemberListServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + List members = memberRepository.findAll(); + + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + + PrintWriter w = response.getWriter(); + w.write(""); + w.write(""); + w.write(" "); + w.write(" Title"); + w.write(""); + w.write(""); + w.write("메인"); + w.write(""); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); +/* + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); +*/ + for (Member member : members) { + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + w.write(" "); + } + w.write(" "); + w.write("
idusernameage
1userA10
" + member.getId() + "" + member.getUsername() + "" + member.getAge() + "
"); + w.write(""); + w.write(""); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberSaveServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberSaveServlet.java new file mode 100644 index 00000000..1c6159b7 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/servlet/MemberSaveServlet.java @@ -0,0 +1,47 @@ +package hello.servlet.web.servlet; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.PrintWriter; + +@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save") +public class MemberSaveServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("MemberSaveServlet.service"); + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + PrintWriter w = response.getWriter(); + + w.write("\n" + + "\n" + + " \n" + + "\n" + + "\n" + + "성공\n" + + "
    \n" + + "
  • id="+member.getId()+"
  • \n" + + "
  • username="+member.getUsername()+"
  • \n" + + "
  • age="+member.getAge()+"
  • \n" + + "
\n" + + "메인\n" + + "\n" + + ""); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberFormServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberFormServlet.java new file mode 100644 index 00000000..01ead1c9 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberFormServlet.java @@ -0,0 +1,21 @@ +package hello.servlet.web.servletmvc; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form") +public class MvcMemberFormServlet extends HttpServlet { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String viewPath = "/WEB-INF/views/new-form.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberListServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberListServlet.java new file mode 100644 index 00000000..56ef49e9 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberListServlet.java @@ -0,0 +1,30 @@ +package hello.servlet.web.servletmvc; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.List; + +@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members") +public class MvcMemberListServlet extends HttpServlet { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + List members = memberRepository.findAll(); + + request.setAttribute("members", members); + + String viewPath = "/WEB-INF/views/members.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberSaveServlet.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberSaveServlet.java new file mode 100644 index 00000000..31f9428d --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/servletmvc/MvcMemberSaveServlet.java @@ -0,0 +1,34 @@ +package hello.servlet.web.servletmvc; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save") +public class MvcMemberSaveServlet extends HttpServlet { + + MemberRepository memberRepository = MemberRepository.getInstance(); + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + + //Model에 데이터 보관. + request.setAttribute("member", member); + + String viewPath = "/WEB-INF/views/save-result.jsp"; + RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); + dispatcher.forward(request, response); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/old/MyHttpRequestHandler.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/old/MyHttpRequestHandler.java new file mode 100644 index 00000000..b3aba1b8 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/old/MyHttpRequestHandler.java @@ -0,0 +1,17 @@ +package hello.servlet.web.springmvc.old; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.HttpRequestHandler; + +import java.io.IOException; + +@Component("/springmvc/request-handler") +public class MyHttpRequestHandler implements HttpRequestHandler { + @Override + public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + System.out.println("MyHttpRequestHandler.handleRequest"); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/old/OldController.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/old/OldController.java new file mode 100644 index 00000000..76de480c --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/old/OldController.java @@ -0,0 +1,16 @@ +package hello.servlet.web.springmvc.old; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +@Component("/springmvc/old-controller") +public class OldController implements Controller { + @Override + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + System.out.println("OldController.handleRequest"); + return new ModelAndView("new-form"); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v1/SpringMemberListControllerV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v1/SpringMemberListControllerV1.java new file mode 100644 index 00000000..c993f3a2 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v1/SpringMemberListControllerV1.java @@ -0,0 +1,25 @@ +package hello.servlet.web.springmvc.v1; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import java.util.List; + +@Controller +public class SpringMemberListControllerV1 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @RequestMapping("/springmvc/v1/members") + public ModelAndView process() { + List members = memberRepository.findAll(); + ModelAndView mv = new ModelAndView("members"); + mv.addObject("members", members); + + return mv; + + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v1/SpringMemberSaveControllerV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v1/SpringMemberSaveControllerV1.java new file mode 100644 index 00000000..931a40a8 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v1/SpringMemberSaveControllerV1.java @@ -0,0 +1,30 @@ +package hello.servlet.web.springmvc.v1; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import java.util.Map; + +@Controller +public class SpringMemberSaveControllerV1 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @RequestMapping("/springmvc/v1/members/save") + public ModelAndView process(HttpServletRequest request, HttpServletResponse response) { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + ModelAndView mv = new ModelAndView("save-result"); + mv.addObject("member", member); + return mv; + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v1/SpringMemverFormControllerV1.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v1/SpringMemverFormControllerV1.java new file mode 100644 index 00000000..10b0cd90 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v1/SpringMemverFormControllerV1.java @@ -0,0 +1,14 @@ +package hello.servlet.web.springmvc.v1; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +@Controller +public class SpringMemverFormControllerV1 { + + @RequestMapping("/springmvc/v1/members/new-form") + public ModelAndView process() { + return new ModelAndView("new-form"); + } +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v2/SpringMemberControllerV2.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v2/SpringMemberControllerV2.java new file mode 100644 index 00000000..a9337bd6 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v2/SpringMemberControllerV2.java @@ -0,0 +1,50 @@ +package hello.servlet.web.springmvc.v2; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import java.util.List; + +@Controller +@RequestMapping("/springmvc/v2/members") +public class SpringMemberControllerV2 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @RequestMapping("/new-form") + public ModelAndView newForm() { + return new ModelAndView("new-form"); + } + + + @RequestMapping() + public ModelAndView save() { + List members = memberRepository.findAll(); + ModelAndView mv = new ModelAndView("members"); + mv.addObject("members", members); + + return mv; + + } + + + @RequestMapping("/save") + public ModelAndView members(HttpServletRequest request, HttpServletResponse response) { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); + + ModelAndView mv = new ModelAndView("save-result"); + mv.addObject("member", member); + return mv; + } + + +} diff --git a/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v3/SpringMemberControllerV3.java b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v3/SpringMemberControllerV3.java new file mode 100644 index 00000000..c5e7d377 --- /dev/null +++ b/9 WEEK/servlet/src/main/java/hello/servlet/web/springmvc/v3/SpringMemberControllerV3.java @@ -0,0 +1,43 @@ +package hello.servlet.web.springmvc.v3; + +import hello.servlet.domain.member.Member; +import hello.servlet.domain.member.MemberRepository; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@Controller +@RequestMapping("/springmvc/v3/members") +public class SpringMemberControllerV3 { + + private MemberRepository memberRepository = MemberRepository.getInstance(); + + @GetMapping("/new-form") + public String newForm() { + return "new-form"; + } + + + @PostMapping("/save") + public String save(@RequestParam("username") String username, @RequestParam("age") int age, Model model) { + Member member = new Member(username, age); + memberRepository.save(member); + model.addAttribute("member", member); + return "save-result"; + } + + + @GetMapping("") + public String members(Model model) { + List members = memberRepository.findAll(); + model.addAttribute("members", members); + return "members"; + } + + +} diff --git a/9 WEEK/servlet/src/main/resources/application.properties b/9 WEEK/servlet/src/main/resources/application.properties new file mode 100644 index 00000000..df7a3be0 --- /dev/null +++ b/9 WEEK/servlet/src/main/resources/application.properties @@ -0,0 +1,5 @@ +logging.level.org.apache.coyote.http11=debug + + +spring.mvc.view.prefix=/WEB-INF/views/ +spring.mvc.view.suffix=.jsp \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/webapp/WEB-INF/views/members.jsp b/9 WEEK/servlet/src/main/webapp/WEB-INF/views/members.jsp new file mode 100644 index 00000000..d9faff4a --- /dev/null +++ b/9 WEEK/servlet/src/main/webapp/WEB-INF/views/members.jsp @@ -0,0 +1,27 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> + + + + Title + + +메인 + + + + + + + + + + + + + + + +
idusernameage
${item.id}${item.username}${item.age}
+ + \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/webapp/WEB-INF/views/new-form.jsp b/9 WEEK/servlet/src/main/webapp/WEB-INF/views/new-form.jsp new file mode 100644 index 00000000..39d9e9b7 --- /dev/null +++ b/9 WEEK/servlet/src/main/webapp/WEB-INF/views/new-form.jsp @@ -0,0 +1,15 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + Title + + + +
+ username: + age: + +
+ + \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/webapp/WEB-INF/views/save-result.jsp b/9 WEEK/servlet/src/main/webapp/WEB-INF/views/save-result.jsp new file mode 100644 index 00000000..d3c0ed84 --- /dev/null +++ b/9 WEEK/servlet/src/main/webapp/WEB-INF/views/save-result.jsp @@ -0,0 +1,15 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + + +성공 +
    +
  • id=${member.id}
  • +
  • username=${member.username}
  • +
  • age=${member.age}
  • +
+메인 + + \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/webapp/basic.html b/9 WEEK/servlet/src/main/webapp/basic.html new file mode 100644 index 00000000..813a0b8e --- /dev/null +++ b/9 WEEK/servlet/src/main/webapp/basic.html @@ -0,0 +1,40 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/webapp/basic/hello-form.html b/9 WEEK/servlet/src/main/webapp/basic/hello-form.html new file mode 100644 index 00000000..a712358f --- /dev/null +++ b/9 WEEK/servlet/src/main/webapp/basic/hello-form.html @@ -0,0 +1,14 @@ + + + + + Title + + +
+ username: + age: + +
+ + \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/webapp/index.html b/9 WEEK/servlet/src/main/webapp/index.html new file mode 100644 index 00000000..22f9745d --- /dev/null +++ b/9 WEEK/servlet/src/main/webapp/index.html @@ -0,0 +1,86 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/webapp/jsp/members.jsp b/9 WEEK/servlet/src/main/webapp/jsp/members.jsp new file mode 100644 index 00000000..3a036b59 --- /dev/null +++ b/9 WEEK/servlet/src/main/webapp/jsp/members.jsp @@ -0,0 +1,36 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="hello.servlet.domain.member.Member" %> +<%@ page import="java.util.List" %> +<%@ page import="hello.servlet.domain.member.MemberRepository" %> +<% + MemberRepository memberRepository = MemberRepository.getInstance(); + + List members = memberRepository.findAll(); +%> + + + + Title + + +메인 + + + + + + + + <% + for (Member member : members) { + out.write(" "); + out.write(" "); + out.write(" "); + out.write(" "); + out.write(" "); + } + %> + +
idusernameage
" + member.getId() + "" + member.getUsername() + "" + member.getAge() + "
+ + \ No newline at end of file diff --git a/9 WEEK/servlet/src/main/webapp/jsp/members/new-form.jsp b/9 WEEK/servlet/src/main/webapp/jsp/members/new-form.jsp new file mode 100644 index 00000000..705255aa --- /dev/null +++ b/9 WEEK/servlet/src/main/webapp/jsp/members/new-form.jsp @@ -0,0 +1,13 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Title + + +
+ username: + age: + +
+ + diff --git a/9 WEEK/servlet/src/main/webapp/jsp/members/save.jsp b/9 WEEK/servlet/src/main/webapp/jsp/members/save.jsp new file mode 100644 index 00000000..6a102d8d --- /dev/null +++ b/9 WEEK/servlet/src/main/webapp/jsp/members/save.jsp @@ -0,0 +1,27 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ page import="hello.servlet.domain.member.Member" %> +<%@ page import="hello.servlet.domain.member.MemberRepository" %> +<% + MemberRepository memberRepository = MemberRepository.getInstance(); + //request, response는 지원함 + System.out.println("MemberSaveServlet.service"); + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + + Member member = new Member(username, age); + memberRepository.save(member); +%> + + + Title + + + 성공 +
    +
  • id=<%=member.getId()%>
  • +
  • username=<%=member.getUsername()%>
  • +
  • age=<%=member.getAge()%>
  • +
+ 메인 + + diff --git a/9 WEEK/servlet/src/test/java/hello/servlet/ServletApplicationTests.java b/9 WEEK/servlet/src/test/java/hello/servlet/ServletApplicationTests.java new file mode 100644 index 00000000..9df6b76e --- /dev/null +++ b/9 WEEK/servlet/src/test/java/hello/servlet/ServletApplicationTests.java @@ -0,0 +1,13 @@ +package hello.servlet; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ServletApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/9 WEEK/servlet/src/test/java/hello/servlet/domain/member/MemberRepositoryTest.java b/9 WEEK/servlet/src/test/java/hello/servlet/domain/member/MemberRepositoryTest.java new file mode 100644 index 00000000..f6c6f6b3 --- /dev/null +++ b/9 WEEK/servlet/src/test/java/hello/servlet/domain/member/MemberRepositoryTest.java @@ -0,0 +1,46 @@ +package hello.servlet.domain.member; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MemberRepositoryTest { + + MemberRepository memberRepository = MemberRepository.getInstance(); + + @AfterEach + void afterEach() { + memberRepository.clearStore(); + } + + @Test + void save() { + Member member = new Member("Hello", 20); + + Member savedMember = memberRepository.save(member); + + Member findMember = memberRepository.findById(savedMember.getId()); + Assertions.assertThat(findMember).isEqualTo(savedMember); + + } + + @Test + void findAll() { + Member member1 = new Member("Hello1", 20); + Member member2 = new Member("Hello2", 20); + + memberRepository.save(member1); + memberRepository.save(member2); + + List memberList = memberRepository.findAll(); + + Assertions.assertThat(memberList.size()).isEqualTo(2); + Assertions.assertThat(memberList).contains(member1, member2); + } + + +} \ No newline at end of file diff --git a/9 WEEK/springmvc/.gitignore b/9 WEEK/springmvc/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/9 WEEK/springmvc/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/9 WEEK/springmvc/SECTION6.md b/9 WEEK/springmvc/SECTION6.md new file mode 100644 index 00000000..d68f5d46 --- /dev/null +++ b/9 WEEK/springmvc/SECTION6.md @@ -0,0 +1,1251 @@ +# 6. 스프링 MVC - 기본 기능 + +--- + +## 로깅 간단히 알아보기 + +운영 시스템에서는 `System.out.println()`같은 시스템 콘솔을 사용해서 필요한 정보를 출력하지 않고, +별도의 로깅 라이브러리를 사용해 로그를 출력한다.
+ + +**로깅 라이브러리**
+스프링 부트 라이브러리를 사용하면 스프링 부트는 로깅 라이브러리(`spring-boot-starter-logging`)가 함께 포함된다. +
스프링 부트 로깅 라이브러리는 기본으로 아래와 같은 라이브러리를 사용한다. +- SLF4J +- Logback + + +로그 라이브러리는 Logback, Log4J, Log4J2 등등 수 많은 라이브러리가 있는데, +그것을 통합해서 인터페이스로 제공하는 것이 바로 SLF4J 라이브러리다. +
SLF4J는 인터페이스이고, 그 구현체로 Logback 같은 로그 라이브러리를 선택하면 된다. +실무에서는 스프링 부트가 기본으로 제공하는 Logback을 대부분 사용한다. + + +**로그 선언** +- `private Logger log = LoggerFactory.getLogger(getClass());` +- `private static final Logger log = LoggerFactory.getLogger(Xxx.class)` +- `@Slf4j` : 롬복 사용 가능 + + +**로그 호출** +- `log.info("hello")` +- `System.out.println("hello")` + + +*LogTestController* +```java +package hello.springmvc.basic; + +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +public class LogTestController { +// private final Logger log = LoggerFactory.getLogger(getClass()); + + @RequestMapping("/log-test") + public String logTest() { + String name = "Spring"; + + System.out.println("name = " + name); + + log.trace("trace log={}", name); + log.debug("debug log={}", name); + log.info("info log={}", name); + log.warn("warn log={}", name); + log.error("error log={}", name); + + + return "ok"; + } +} +``` + +**매핑 정보** +- `@RestController` + - `@Controller`는 반환 값이 `String`이면 뷰 이름으로 인식. 그래서 **뷰를 찾고 뷰가 렌더링**된다. + - `@RestController`는 반환 값으로 뷰를 찾는 것이 아니라, **HTTP 메시지 바디에 바로 입력**한다. + 따라서 실행 결과로 ok 메시지를 받을 수 있다. `@ResponseBody`와 관련이 있다. + + +**테스트** +- 로그가 출력되는 포멧 확인 + - 시간, 로그 레벨, 프로세스 ID, 쓰레드 명, 클래스 명, 로그 메시지 + - 로그 레벨 설정을 변경해 출력 결과 확인 + - LEVEL : `TRACE > DEBUG > INFO > WARN > ERROR` + - 개발 서버는 debug 출력 + - 운영 서버는 info 출력 + - `@Slf4j`로 변경 + +**로그 레벨 설정** + +*application.properties* +``` +logging.level.root=info +logging.level.hello.springmvc=debug +``` + + +**올바른 로그 사용법** +- log.debug("data="+data) + - 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 "data="+data가 실제 실행이 되어 버린다. 결과적으 + 로 문자 더하기 연산이 발생한다. < 사용 X + - log.debug("data={}", data) + - 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않는다. 따라서 앞과 같은 의미없는 연산이 발생하지 + 않는다. + + + +**로그 사용시 장점** +- 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있음. +- 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, +운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있음. +- 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있음. +특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능. +- 성능도 일반 System.out보다 좋음👍. (내부 버퍼링, 멀티 쓰레드 등등) 그래서 실무에서는 꼭 로그를 사용해야 함 + + +--- + + +## 요청 매핑 + + +*MappingController* +```java +package hello.springmvc.basic.requestmapping; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +public class MappingController { + + @RequestMapping("/hello-basic") + public String helloBasic() { + log.info("helloBasic"); + return "ok"; + } +} +``` +- `@RestController` + - `@Controller`는 반환 값이 `String`이면 뷰 이름으로 인식. 따라서 **뷰를 찾고 뷰가 렌더링**됨. + - `@RestController`는 반환 값으로 뷰를 찾는 것이 아니라, **HTTP 메시지 바디에 바로 입력**됨. +- `@RequestMapping("/hello-basic")` + - `/hello-basic` URL 호출이 오면 이 메서드가 실행되도록 메핑. + - 대부분의 속성을 `배열[]`로 제공하므로 다중 설정이 가능 ex) `{"/hello-basic, "/hello-go"}` + + + +**HTTP 메서드**
+`@RequestMapping`에 `method`속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출됨.
+모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE + + +**HTTP 메서드 매핑** +```java + +// @RequestMapping(value = "/mapping-get-v2", method = RequestMethod.GET) + @GetMapping("/mapping-get-v2") //축약 + public String mappingGetV2() { + log.info("mapping-get-v2"); + return "ok"; + } +``` +만약 여기에 GET 외의 요청, 즉 POST 요청을 하면 스프링 MVC는 HTTP 405 상태코드(Method Not Allowed)를 반환.
+HTTP 메서드를 축약한 애노테이션을 사용하는 것이 더 직관적임. + + +**PathVariable(경로변수) 사용** +```java + @GetMapping("/mapping/{userId}") + public String mappingPath(@PathVariable("userId") String data) { + log.info("mappingPath userId={}", data); + return "ok"; + } +``` +최근 HTTP API는 다음과 같은 리소스 경로에 식별자를 넣는 스타일을 선호. +- `/mapping/userA` +- `/users/1` +- `@RequestMapping`은 URL 경로를 템플릿화 할 수 있는데, `@PathVariable`을 사용하면 매칭 되는 부분을 편리하게 조회 가능. +- `@PathVariable`의 이름과 파라미터 이름이 같으면 생략 가능. + + +**PathVariable 다중 사용** +```java + @GetMapping("/mapping/users/{userId}/orders/{orderId}") + public String mappingPath(@PathVariable String userId, @PathVariable String orderId) { + log.info("mappingPath userId={}, orderId={}", userId, orderId); + return "ok"; + } +``` + + +*특정 파라미터 조건 매핑* +```java + @GetMapping(value = "/mapping-param", params = "mode=debug") + public String mappingParam() { + log.info("mappingParam"); + return "ok"; + } +``` +실행 +- http://localhost:8080/mapping-param?mode=debug
+잘 사용하지는 않는다 + + +*특정 헤더 조건 매핑* +```java + @GetMapping(value = "/mapping-header", headers = "mode=debug") + public String mappingHeader() { + log.info("mappingHeader"); + return "ok"; + } +``` +HTTP 헤더를 사용해야 한다. + + +*미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume* +```java + @PostMapping(value = "/mapping-consume", consumes = "application/json") + public String mappingConsume() { + log.info("mappingConsume"); + return "ok"; + } +``` +HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑.
+만약 맞지 않으면 HTTP 415 상태코드(Unsupported Media Type)을 반환. + +예시) consumes +``` +consumes = "text/plain" +consumes = {"text/plain", "application/*"} +consumes = MediaType.TEXT_PLAIN_VALUE +``` + + + + +*미디어 타입 조건 매핑 - HTTP 요청 Accept, produce* +```java + @PostMapping(value = "/mapping-produce", produces = "text/html") + public String mappingProduce() { + log.info("mappingProduce"); + return "ok"; + } +``` +HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑.
+만약 맞지 않으면 HTTP 406 상태코드(Not Acceptable)을 반환. + +예시) +``` +produces = "text/plain" +produces = {"text/plain", "application/*"} +produces = MediaType.TEXT_PLAIN_VALUE +produces = "text/plain;charset=UTF-8" +``` + + + + +--- + +## 요청 매핑 - API 예시 + +**회원 관리 API** +- 회원 목록 조회 : GET `/users` +- 회원 등록 : POST `/users` +- 회원 조회 : GET `/users/{userId}` +- 회원 수정 : PATCH `/users/{userId}` +- 회원 삭제 : DELETE `/users/{userId}` + + +*MappingClassController* +```java +package hello.springmvc.basic.requestmapping; + +import org.springframework.web.bind.annotation.*; + +@RequestMapping("/mapping/users") +@RestController +public class MappingClassController { + + + @GetMapping + public String user() { + return "get users"; + } + + @PostMapping + public String addUser() { + return "post user"; + } + + @GetMapping("/{userId}") + public String findUser(@PathVariable("userId") String userId) { + return "get userId=" + userId; + } + + @PatchMapping("/{userId}") + public String updateUser(@PathVariable("userId") String userId) { + return "update userId=" + userId; + } + + @DeleteMapping("/{userId}") + public String deleteUser(@PathVariable("userId") String userId) { + return "delete userId=" + userId; + } + +} +``` + + + +--- + +## HTTP 요청 - 기본, 헤더 조회 +애노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원한다. + +*RequestHeaderController* +```java +package hello.springmvc.basic.request; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Locale; + +@Slf4j +@RestController +public class RequestHeaderController { + + @RequestMapping("/headers") + public String headers( + HttpServletRequest request, + HttpServletResponse response, + HttpMethod httpMethod, + Locale locale, + @RequestHeader MultiValueMap headerMap, + @RequestHeader("host") String host, + @CookieValue(value = "myCookie", required = false) String cookie + ) { + log.info("request={}", request); + log.info("response={}", response); + log.info("httpMethod={}", httpMethod); + log.info("locale={}", locale); + log.info("headerMap={}", headerMap); + log.info("header host={}", host); + log.info("myCookie={}", cookie); + return "ok"; + } + +} +``` + +- `HttpServletRequest` +- `HttpServletResponse` +- `HttpMethod` : HTTP 메서드를 조회. +- `Locale` : Locale 정보를 조회. +- `@RequestHeader MultiValueMap headerMap` : 모든 HTTP 헤더를 MultiValueMap 형식으로 조회. +- `@RequestHeader("host") String host` : 특정 HTTP 헤더를 조회. + - 속성 + - 필수 값 여부 : `required` + - 기본 값 속성 : `defaultValue` +- `@CookieValue(value = "myCookie", required = false) String cookie` : 특정 쿠키를 조회 + - 속성 + - 필수 값 여부 : `required` + - 기본 값 속성 : `defaultValue` +- `MultiValueMap` : MAP과 유사한데, 하나의 키에 여러 값을 받을 수 있음. + - HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용. + - **keyA=value1&keyA=value2** + + +--- + +## HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form + +### HTTP 요청 데이터 조회 +**클라이언트에서 서버로 요청 데이터를 전달할 때 3가지 방법을 주로 사용.** +- GET - 쿼리 파라미터 +- POST - HTLM Form +- HTTP message body + +### 요청 파라미터 - 쿼리 파라미터, HTML Form +`HttpServletRequest`의 `request.getParameter()`를 사용하면 다음 두가지 요청 파라미터 조회 가능. +- GET - 쿼리 파라미터 전송 + - `http://localhost:8080/request-param?username=hello&age=20` +- POST, HTML Form 전송 +``` +POST /request-param ... +content-type: application/x-www-form-urlencoded +username=hello&age=20 +``` + + +*RequestParamController* +```java +package hello.springmvc.basic.request; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.io.IOException; + +@Slf4j +@Controller +public class RequestParamController { + + @RequestMapping("/request-param-v1") + public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + log.info("username={}, age={}", username, age); + + response.getWriter().write("OK"); + + } + +} +``` + + +**requset.getParameter()** +- 단순히 HttpServletRequest가 제공하는 방식으로 쿼리 파라미터를 조회함 + +**Post Form 페이지 생성**
+테스트용 HTML Form
+*hello-form.html* +```html + + + + + Title + + +
+ username: + age: + +
+ + +``` + + +--- + + +## HTTP 요청 파라미터 - @RequsetParam +스프링이 제공하는 `@RequestParma`을 사용하면 요청 파라미터를 매우 편리하게 사용 가능. + +*requestParamV2 method* +```java + @ResponseBody + @RequestMapping("/request-param-v2") + public String requestParamV2( + @RequestParam("username") String memberName, + @RequestParam("age") int memberAge) { + log.info("username={}, age={}", memberName, memberAge); + return "ok"; + } +``` +- `@RequestParam` : 파라미터 이름으로 바인딩 +- `@ResponseBody` : View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력 + +**@RequestParam**의 `name(value)` 속성이 파라미터 이름으로 사용 +- @RequestParam("username") String memberName -> request.getParameter("username") + + +*requestParamV3 method* +```java + @ResponseBody + @RequestMapping("/request-param-v3") + public String requestParamV3( + @RequestParam String username, + @RequestParam int age) { + log.info("username={}, age={}", username, age); + return "ok"; + } +``` +HTTP 파라미터 이름이 변수 이름과 같으면 `@Request(name ="")` 생략 가능 + + +*requestParamV4 method* +```java + @ResponseBody + @RequestMapping("/request-param-v4") + public String requestParamV4(String username, int age) { + log.info("username={}, age={}", username, age); + return "ok"; + } +``` +`String` , `int` , `Integer` 등의 단순 타입이면 `@RequestParam`도 생략 가능 + + +*requestParamRequired method - 파라미터 필수 여부* +```java +/** + * @RequestParam.required + * /request-param-required -> username이 없으므로 예외 + * + * 주의! + * /request-param-required?username= -> 빈문자로 통과 + * + * 주의! + * /request-param-required + * int age -> null을 int에 입력하는 것은 불가능, 따라서 Integer 변경해야 함(또는 다음에 나오는 +defaultValue 사용) + */ + @ResponseBody + @RequestMapping("/request-param-required") + public String requestParamV5( + @RequestParam(required = true) String username, + @RequestParam(required = false) Integer age //int는 null이 못들어감 다만 Integer은 객체라 가능 + ) { + log.info("username={}, age={}", username, age); + return "ok"; + } +``` +- `@RequestParam.required` + - 파라미터 필수 여부 + - 기본값이 파라미터 필수(`true`) +- `/request-param-required` 요청 + - `username`이 없으므로 400 예회 발생 + +**주의! - 파라미터 이름만 사용**
+`/request-param-required?username=`
+파라미터 이름만 있고 값이 없는 경우 빈문자로 통과 + + +**주의! - 기본형(primitive)에 null 입력** +- /request-param 요청 +- @RequestParam(required = false) int age + +null 을 int 에 입력하는 것은 불가능(500 예외 발생)
+따라서 null 을 받을 수 있는 Integer 로 변경하거나, 또는 다음에 나오는 defaultValue 사용 + + +*requestParamDefault method* +```java + @ResponseBody + @RequestMapping("/request-param-default") + public String requestParamDefault( + @RequestParam(required = true, defaultValue = "guest") String username, + @RequestParam(required = false , defaultValue = "-1") Integer age + ) { + log.info("username={}, age={}", username, age); + return "ok"; + } +``` +- 파라미터에 값이 없는 경우 `defaultValue` 를 사용하면 기본 값을 적용할 수 있음 +- 이미 기본 값이 있기 때문에 `required` 는 의미가 없음 + +defaultValue 는 빈 문자의 경우에도 설정한 기본 값이 적용됨.
+`/request-param-default?username=` + + +*requestParam method - 파라미터를 Map으로 조회* +```java +/** + * @RequestParam Map, MultiValueMap + * Map(key=value) + * MultiValueMap(key=[value1, value2, ...]) ex) (key=userIds, value=[id1, id2]) + */ + @ResponseBody + @RequestMapping("/request-param-map") + public String requestParamMap(@RequestParam Map paramMap) { + log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age")); + return "ok"; + } +``` +파라미터를 Map, MultiValueMap으로 조회 가능 +- `@RequestParam Map` + - `Map(key=value)` +- `@RequestParam MultiValueMap` + - `MultiValueMap(key=[value1,value2,....] ex) (key=userIds, value=[id1, id2])` + + +파라미터의 값이 1개가 확실하다면 Map 을 사용해도 되지만, 그렇지 않다면 MultiValueMap 을 사용 + + + +--- + + +# HTTP 요청 파라미터 - @ModelAttribute +스프링은 `@ModelAttribute` 기능을 제공함 + +요청 파라미터를 바인딩 받을 객체 생성 + +*HelloData* +```java +package hello.springmvc.basic; + + +import lombok.Data; + +@Data +public class HelloData { + private String username; + private int age; +} +``` +- `@Data` + - `@Getter`, `@Setter`, `@EqualsAndHashCode`, `@RequiredArgsConstructor`를 자동으로 적용해줌 + + +*modelAttributeV1 method* +```java + @ResponseBody + @RequestMapping("/model-attribute-v1") + public String modelAttributeV1(@ModelAttribute HelloData helloData) { + log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); + log.info("helloData={}", helloData); + return "ok"; + } +``` +자동으로 `HelloData` 객체가 생성되고, 요청 파라미터 값도 모두 들어간다.
+ +스프링 MVC는 `@ModelAttribute`가 있으면 다음을 실행한다. +- `HelloData` 객체 생성 +- 요청 파라미터의 이름으로 `HelloData`객체의 프로퍼티를 조사, 그리고 해당 프로퍼티의 setter를 호출해 파라미터의 값 입력(바인딩). +- ex) 파라미터 이름이 `username`이면 `setUsername()`메서드를 찾아 호출해 값을 입력 + + +**프로퍼티**
+객체에 `getUsername()`, `setUsername()`메서드가 있으면, 이 객체는 `username`이라는 프로퍼티를 가짐.
+`username`프로퍼티의 값을 변경하면 `setUsername()`이 호출, 조회하면 `getUsername()`이 호출됨. +```java +class HelloData { + getUsername();setUsername(); +} +``` + + +*modelAttributeV2 - @ModelAttribute 생략* +```java + @ResponseBody + @RequestMapping("/model-attribute-v2") + public String modelAttributeV2( HelloData helloData) { + log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); + log.info("helloData={}", helloData); + return "ok"; + } +``` + +`@ModelAttribute`는 생략 가능. 하지만 `@RequestParam`도 생략할 수 있어 혼란이 발생할 수 있음. + + +스프링은 해당 생략시 다음과 같은 규칙을 적용. +- `String`, `int`, `Integer` 같은 단순 타입 = @RequestParam +- 나머지 = @ModelAttribute (argument resolver 로 지정해둔 타입 외) + + +--- + + +# HTTP 요청 메시지 - 단순 텍스트 + +- **HTTP message body에 데이터를 직접 담아서 요청** + - HTTP API에서 주로 사용, JSON, XML, TEXT + - 데이터 형식은 주로 JSON 사용 + - POST, PUT, PATCH + +요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 +`@RequestParam`, `@ModelAttribute`를 사용할 수 없음.(HTTP Form형식으로 전달되는 경우 요청 파라미터로 인정됨) + + +HTTP 메시지 바디에 데이터를 `InputStream`을 사용해 직접 읽을 수 있음. + +*RequestBodyStringController* +```java +package hello.springmvc.basic.request; + +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.stereotype.Controller; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.nio.charset.StandardCharsets; + +@Slf4j +@Controller +public class RequestBodyStringController { + + @PostMapping("/request-body-string-v1") + public void requestBodyStringV1(HttpServletRequest request, HttpServletResponse response) throws IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + + log.info("messageBody={}", messageBody); + response.getWriter().write("ok"); + } +} +``` + + +*requestBodyStringV2 method - Input, Output Stream, Reader* +```java + @PostMapping("/request-body-string-v2") + public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException { + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + log.info("messageBody={}", messageBody); + responseWriter.write("ok"); + } +``` + +**스프링 MVC는 다음 파라미터를 지원** +- InputStream(Reader) : HTTP 요청 메시지 바디의 내용을 직접 조회 +- OutputStream(Writer) : HTTP 응답 메시지의 바디에 직접 결과 출력 + + +*requestBodyStringV3 method - HttpEntity* +```java + @PostMapping("/request-body-string-v3") + public HttpEntity requestBodyStringV3(HttpEntity httpEntity) throws IOException { + String messageBody = httpEntity.getBody(); + log.info("messageBody={}", messageBody); + return new HttpEntity<>("ok"); + } +``` + +**스프링 MVC는 다음 파라미터를 지원** +- **HttpEntity : HTTP header, body 정보를 편리하게 조회** + - 메시지 바디 정보를 직접 조회 + - 요청 파라미터를 조회하는 기능과 관계 없음! `@RequestParam`X, `@ModelAttribute`X +- **HttpEntity**는 응답에도 사용 가능 + - 메시지 바디 정보를 직접 반환 + - 헤더 정보 포함 가능 + - view 조회X + +`HttpEntity`를 상속받은 다음 객체들도 같은 기능을 제공. +- **RequestEntity** + - HttpMethod, url 정보가 추가, 요청에도 사용. +- **ResponseEntity** + - HTTP 상태 코드 설정 가능, 응답에도 사용 + - `return new ResponseEntity("ok", responseHeaders,HttpStatus.CREATED)` + + +*requestBodyString method - @RequestBody* +```java + @ResponseBody + @PostMapping("/request-body-string-v4") + public String requestBodyStringV4(@RequestBody String messageBody) { + log.info("messageBody={}", messageBody); + return "ok"; + } +``` + +`@RequestBody`
+`@RequestBody`를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회 가능. +헤더 정보가 필요하면 `HttpEntity` or `@RequestHeader`를 사용하면 됨.
+이렇게 메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는 `@RequestParam`, `@ModelAttribute`와 관계 없음! + + +**요청 파라미터 vs HTTP 메시지 바디** +- 요청 파라미터를 조회하는 기능: @RequestParam , @ModelAttribute +- HTTP 메시지 바디를 직접 조회하는 기능: @RequestBody + + +**@ResponseBody**
+@ResponseBody 를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있음. +물론 이 경우에도 view를 사용하지 않음. + + +--- + +## HTTP 요청 메시지 - JSON + + +*RequestBodyJsonController* +```java +package hello.springmvc.basic.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import hello.springmvc.basic.HelloData; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.stereotype.Controller; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@Slf4j +@Controller +public class RequestBodyJsonController { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @PostMapping("/request-body-json-v1") + public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + log.info("messageBody={}", messageBody); + HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); + log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); + + response.getWriter().write("ok"); + } +} +``` + +- HttpServletRequest를 사용해서 직접 HTTP 메시지 바디에서 데이터를 읽고, 문자로 변환. +- 문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper 를 사용해서 자바 객체로 변환. + + +*requestBodyJsonV2 - @RequestBody 문자 변환* +```java + @ResponseBody + @PostMapping("/request-body-json-v2") + public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException { + log.info("messageBody={}", messageBody); + HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); + log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); + return "ok"; + } +``` + +- @RequestBody 를 사용해서 HTTP 메시지에서 데이터를 꺼내고 messageBody에 저장. +- 문자로 된 JSON 데이터인 messageBody 를 objectMapper 를 통해서 자바 객체로 변환. + + +*requestBodyJsonV3 method - @RequestBody 객체 변환* +```java + @ResponseBody + @PostMapping("/request-body-json-v3") + public String requestBodyJsonV3(@RequestBody HelloData helloData) { + log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); + return "ok"; + } +``` + +**@RequestBody 객체 파라미터** +- `@RequestBody HelloData data` +- `@RequestBody` 에 직접 만든 객체를 지정할 수 있음. + +`HttpEntity` , `@RequestBody` 를 사용하면 HTTP 메시지 컨버터가 +HTTP 메시지 바디의 내용을 원하는 문자나 객체 등으로 변환해줌
+HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해주는데, +방금 V2에서 했던 작업을 대신 처리 해줌. + + +**@RequestBody 생략 불가**
+스프링은 `@ModelAttribute` , `@RequestParam` 과 같은 해당 애노테이션을 생략시 다음과 같은 규칙을 적용. +- `String`, `int`, `Integer` 같은 단순 타입 = `@RequestParam` +- 나머지 = `@ModelAttribute` (argument resolver 로 지정해둔 타입 외) +따라서 이 경우 `HelloData`에 `@RequestBody` 를 생략하면 `@ModelAttribute` 가 적용됨.
+`HelloData data` -> `@ModelAttribute HelloData data`
+따라서 생략하면 HTTP 메시지 바디가 아니라 요청 파라미터를 처리하게 됨. + + +*requestBodyJsonV4 method - HttpEntity* +```java + @ResponseBody + @PostMapping("/request-body-json-v4") + public String requestBodyJsonV4(HttpEntity httpEntity) { + HelloData data = httpEntity.getBody(); + log.info("username={}, age={}", data.getUsername(), data.getAge()); + return "ok"; + } +``` +HttpEntity 도 사용 가능 + + +*requestBodyJsonV5 method* +```java + @ResponseBody + @PostMapping("/request-body-json-v5") + public HelloData requestBodyJsonV5(@RequestBody HelloData data) { + log.info("username={}, age={}", data.getUsername(), data.getAge()); + return data; + } +``` +`@ResponseBody`
+응답의 경우에도 @ResponseBody 를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있음.
+이 경우에도 HttpEntity 를 사용해도 됨. +- @RequestBody 요청 + - JSON 요청 HTTP 메시지 컨버터 객체 +- @ResponseBody 응답 + - 객체 HTTP 메시지 컨버터 JSON 응답 + + +--- + + +## HTTP 응답 - 정적 리소스, 뷰 템플릿 +스프링에서 응답 데이터를 만드는 방법은 크게 3가지임. + +- 정적 리소스 +- 뷰 템플릿 <- 동적 리소스 +- HTTP 메시지 + - HTTP API를 제공하는 경우 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON같은 형식으로 데이터를 실어 보냄. + + +### 정적 리소스 +스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공. +- `/static` +- `/public` +- `/resources` +- `/META-INF/resources` + +`src/main/resources`는 리소스를 보관하는 곳이고, 또 클래스패스의 시작 경로임.
+따라서 다음 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공. + + +### 뷰 템플릿 +- 뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달. +- 일반적으로 HTML을 동적으로 생성하는 용도로 사용하지만, 다른 것들도 가능. +뷰 템플릿이 만들 수 있는 것이라면 뭐든지 가능. +- 스프링 부트는 기본 뷰 템플릿 경로를 제공. + +**뷰 템플릿 경로**
+`src/main/resources/templates` + + + +*뷰 템플릿 생성* +```html + + + + + Title + + +

empty

+ + +``` + +*ResponseViewController - 뷰 템플릿을 호출하는 컨트롤러* +```java +package hello.springmvc.basic.response; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +@Controller +public class ResponseViewController { + + @RequestMapping("/response-view-v1") + public ModelAndView responseViewV1() { + ModelAndView mav = new ModelAndView("/response/hello") + .addObject("data", "hello"); + return mav; + } + + @RequestMapping("/response-view-v2") + public String responseViewV2(Model model) { + model.addAttribute("data", "hello"); + return "/response/hello"; + } + + @RequestMapping("/response/hello") + public void responseViewV3(Model model) { + model.addAttribute("data", "hello"); + } +} +``` + +*String을 반환하는 경우 - View or HTTP 메시지*
+- `@ResponseBody가 없을 때` : `response/hello`로 뷰 리졸버가 실행되어서 뷰를 찾고, 렌더링. +- `@ResponseBody가 있을 때`: 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 +`response/hello`라는 문자가 입력됨. + +**Void를 반환하는 경우** +- `@Controller`를 사용하고, `HttpServletResponse`, `OutputStream(Writer)`같은 +HTTP 메시지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용 + - 요청 URL: /response/hello + - 실행: templates/response/hello.html +- 이 방식은 명시성이 너무 떨어지고 이렇게 딱 맞는 경우도 많이 없어서, 권장하지 않음. + +**HTTP 메시지** +`@ResponseBody` , `HttpEntity` 를 사용하면, 뷰 템플릿을 사용하는 것이 아니라, +HTTP 메시지 바디에 직접 응답 데이터를 출력할 수 있음. + + +### Thymeleaf 스프링 부트 설정 + +스프링 부트가 자동으로 ThymeleafViewResolver 와 필요한 스프링 빈들을 등록한다. 그리고 다음 설정도 사용한다. +이 설정은 기본 값 이기 때문에 변경이 필요할 때만 설정하면 된다. + +*application.properties* +``` +spring.thymeleaf.prefix=classpath:/templates/ +spring.thymeleaf.suffix=.html +``` + +--- + + +## HTTP 응답 - HTTP API, 메시지 바디에 직접 입력 +HTTP API를 제공하는 경우 HTML이 아니라 데이터를 전달해야 하기 떄문에, HTTP 메시지 바디에 JSON 같은 데이터를 담아야 한다. + + +*ResponseBodyController* +```java +package hello.springmvc.basic.response; + +import hello.springmvc.basic.HelloData; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.io.IOException; + +@Slf4j +@Controller +public class ResponseBodyController { + @GetMapping("/response-body-string-v1") + public void responseBodyV1(HttpServletResponse response) throws IOException + { + response.getWriter().write("ok"); + } + + @GetMapping("/response-body-string-v2") + public ResponseEntity responseBodyV2() { + return new ResponseEntity<>("ok", HttpStatus.OK); + } + @ResponseBody + @GetMapping("/response-body-string-v3") + public String responseBodyV3() { + return "ok"; + } + @GetMapping("/response-body-json-v1") + public ResponseEntity responseBodyJsonV1() { + HelloData helloData = new HelloData(); + helloData.setUsername("userA"); + helloData.setAge(20); + return new ResponseEntity<>(helloData, HttpStatus.OK); + } + @ResponseStatus(HttpStatus.OK) + @ResponseBody + @GetMapping("/response-body-json-v2") + public HelloData responseBodyJsonV2() { + HelloData helloData = new HelloData(); + helloData.setUsername("userA"); + helloData.setAge(20); + return helloData; + } +} +``` + +**responseBodyV1** +- 서블릿을 직접 다룰 때 처럼 `HttpServletResponse`객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지를 전달.
+`response.getWriter().write("ok")` + +**responseBodyV2** +- `ResponseEntity`엔티티는 `HttpEntity`를 상속 받았는데, `HttpEntity`는 +HTTP 메시지의 헤더, 바디 정보를 가지고 있음. `ResponseEntity`는 여기에 더해서 HTTP 응답 코드를 설정할 수 있음.
+`HttpStatus.CREATED` 로 변경하면 201 응답이 나가는 것을 확인할 수 있다. + +**responseBodyV3** +- `@ResponseBody` 를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력할 수 있음. +`ResponseEntity` 도 동일한 방식으로 동작한다. + +**responseBodyJsonV1** +- `ResponseEntity`를 반환. HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환. + +**responseBodyJsonV2** +- `ResponseEntity`는 HTTP 응답 코드를 설정할 수 있는데, `@ResponseBody`를 사용하면 이런 것을 설정하기 까다로우. +`@ResponseStatus(HttpStatus.OK)` 애노테이션을 사용하면 응답 코드도 지정 가능. + +물론 애노테이션이기 때문에 응답 코드를 동적으로 변경할 수는 없음. +프로그램 조건에 따라서 동적으로 변경하려면 `ResponseEntity`를 사용. + + +**@RestController**
+`@Controller` 대신에 `@RestController`애노테이션을 사용하면, 해당 컨트롤러에 모두 `@ResponseBody`가 적용된다. +따라서 뷰 템플릿을 사용하는 것이 아니라, HTTP메시지 바디에 직접 데이터를 입력한다. 그대로 RestAPI를 만들 때 사용하는 컨트롤러이다. + +> 참고 : `@RestController` 안에 `@ResponseBody`가 적용돼 있음. + + +--- + + +## HTTP 메시지 컨버터 + +뷰 템플릿으로 HTML을 생성해 응답하는 것이 아닌, HTTP API처럼 JSON 데이터를 +HTTP메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 이용하면 편리하다. + +**@ResponseBody 사용 원라** +![S6-1.png](img%2FS6-1.png) + +- `@ResponseBody` 사용 + - HTTP의 Body에 문자 내용을 직접 반환 + - `viewResolver` 대신에 `HttpMessageConverter` 동작 + - 기본 문자처리 : `StringHttpMessageConverter` + - 기본 객체처리 : `MappingJackson2HttpMessageConverter` + - byte처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록돼 있음. + + +**스프링 MVC는 다음의 경우에 HTTP메시지 컨버터를 적용.** +- HTTP 요청 : `@RequestBody`, `HttpEntity(RequestEntity)` +- HTTP 응답 : `@ResponseBody`, `HttpEntity(ResponseEntity)` + + +*HTTP 메시지 컨버터 인터페이스* +```java +//생략 +``` +HTTP 메시지 컨버터는 HTTP요청, HTTP응답 둘 다 사용됨. +- `canRead()`, `canWrite()` : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크 +- `read()`, `write()` : 메시지 컨버터를 통해 메시지를 읽고 쓰는 기능 + + +**스프링 부트 기본 메시지 컨버터** + ``` +0 = ByteArrayHttpMessageConverter +1 = StringHttpMessageConverter +2 = MappingJackson2HttpMessageConverter +``` +- ByteArrayHttpMessageConverter : `byte[]`데이터를 처리 + - 클래스 타입 : `byte[]`, 미디어타입 : `*/*` +- `StringHttpMessageConverter` : `String`문자로 데이터 처리 + - 클래스 타입 : `String`, 미디어타입 : `*/*` +- `MappingJackson2HttpMessageConverter` : application/json + - 클래스 타입 : 객체 또는 `HashMap`, 미디어타입 : `application/json` 관련 + + +*StringHttpMessageConverter* +``` +content-type: application/json +@RequestMapping +void hello(@RequestBody String data) {} +``` + +*MappingJackson2HttpMessageConverter* +``` +content-type: application/json +@RequestMapping +void hello(@RequestBody HelloData data) {} +``` + +*?* +``` +content-type: text/html +@RequestMapping +void hello(@RequestBody HelloData data) {} +``` + +**HTTP 요청 데이터 읽기** +- HTTP 요청이 오고, 컨트롤러에서 `@RequestBody`. `HttpEntity`파라미터를 사용 +- 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 `canRead()` 호출. + - 대상 클래스 타입을 지원하는지 + - HTTP 요청의 Content-Type 미디어 타입을 지원하는지 +- `canRead()`조건을 만족하면 `read()`를 호출해 객체 생성, 반환함. + + +**HTTP 응답 데이터 생성** +- 컨트롤러에서 `@ResponseBody`, `HttpEntity`로 값이 반환. +- 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 `canWrite()`를 호출. + - 대상 클래스 타입을 지원하는지 + - HTTP 요청의 Accept 미디어 타입을 지원하는지 +- `canWrite()`조건을 만족하면 `write()`를 호출해 HTTP응답 메시지 바디에 데이터를 생성. + + +--- + + +## 요청 메핑 핸들러 어댑터 구조 + +HTTP 메시지 컨버터는 스프링 MVC에서 애노테이션 기반의 컨트롤러, 즉 `@RequestMapping`을 처리하는 +핸들러 어댑터인 `RequestMappingHandlerAdpater`(요청 메핑 핸들러)에 있다. + + +**RequestMappingHandlerAdapter 동작 방식** +![S6-2.png](img%2FS6-2.png) + +**ArgumentResolver**
+애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있음. +`HttpServletRequest`, `Model`은 물론이고, `@RequsetParam`, `@ModelAttribute` 같은 애노테이션 +그리고 `@RequestBody`, `HttpEntity`같은 HTTP 메시지를 처리하는 부분까지 매우 큰 유연함을 보여줌.
+파라미터를 유연하게 처리할 수 있는 이유가 바로 `ArgumentResolver` 덕분임. + +애노테이션 기반 컨트롤러를 처리하는 `RequestMappingHandlerAdapter`는 바로 이 `ArgumentResolver`를 호출해 +컨트롤러(핸들러)가 필요로하는 다양한 파라미터 값(객체)를 생성함. 그리고 이렇게 파라미터의 값이 모두 준비되면 +컨트롤러를 호출해 값을 넘겨줌 + + +**ReturnValueHandler** +`HandlerMethodReturnValueHandler`를 줄여서 `ReturnValueHandler`라 부름
+`ArgumentResolver`와 비슷한데, 이것은 응답 값을 변환하고 처리.
+컨트롤러에서 `String`으로 뷰 이름을 반환해도, 동작하는 이유가 바로 `ReturnValueHandler`때문임. + +스프링은 10여개가 넘는 `ReturnValueHandler` 를 지원.
+예) `ModelAndView` , `@ResponseBody` , `HttpEntity` , `String` + + +### HTTP 메시지 컨버터 + +**HTTP 메시지 컨버터 위치** +![S6-3.png](img%2FS6-3.png) + +HTTP 메시지 컨버터를 사용하는 `@RequestBody`도 컨트롤러가 필요로 하는 파라미터의 값에 사용.
+`@RequestBody`의 경우 컨트롤러의 반환 값을 이용. + + +**요청의 경우** +- `@RequestBody`를 처리하는 `ArgumentResolver`가 있고, `HttpEntity`를 처리하는`ArgumentResolver`가 있음. +이 `ArgumentResolver` 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성 + + +**응답의 경우** +- `@ResponseBody`와 `HttpEntity`를 처리하는 `ReturnValueHandler`가 있음. +여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만듦 + +스프링 MVC는 `@RequestBody`, `@ResponseBody`가 있으면 `RequestResponseBodyMethodProcessor()`를, +`HttpEntity`가 있으면 `HttpEntityMethodProcessor()`를 사용 + + + + + + + + + +--- diff --git a/9 WEEK/springmvc/build.gradle b/9 WEEK/springmvc/build.gradle new file mode 100644 index 00000000..a65c94f1 --- /dev/null +++ b/9 WEEK/springmvc/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.1' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'hello' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/9 WEEK/springmvc/gradle/wrapper/gradle-wrapper.jar b/9 WEEK/springmvc/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/9 WEEK/springmvc/gradle/wrapper/gradle-wrapper.jar differ diff --git a/9 WEEK/springmvc/gradle/wrapper/gradle-wrapper.properties b/9 WEEK/springmvc/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1af9e093 --- /dev/null +++ b/9 WEEK/springmvc/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/9 WEEK/springmvc/gradlew b/9 WEEK/springmvc/gradlew new file mode 100644 index 00000000..1aa94a42 --- /dev/null +++ b/9 WEEK/springmvc/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/9 WEEK/springmvc/gradlew.bat b/9 WEEK/springmvc/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/9 WEEK/springmvc/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/9 WEEK/springmvc/img/S6-1.png b/9 WEEK/springmvc/img/S6-1.png new file mode 100644 index 00000000..304d12e6 Binary files /dev/null and b/9 WEEK/springmvc/img/S6-1.png differ diff --git a/9 WEEK/springmvc/img/S6-2.png b/9 WEEK/springmvc/img/S6-2.png new file mode 100644 index 00000000..d640eb3e Binary files /dev/null and b/9 WEEK/springmvc/img/S6-2.png differ diff --git a/9 WEEK/springmvc/img/S6-3.png b/9 WEEK/springmvc/img/S6-3.png new file mode 100644 index 00000000..482dd63a Binary files /dev/null and b/9 WEEK/springmvc/img/S6-3.png differ diff --git a/9 WEEK/springmvc/settings.gradle b/9 WEEK/springmvc/settings.gradle new file mode 100644 index 00000000..5015d018 --- /dev/null +++ b/9 WEEK/springmvc/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'springmvc' diff --git a/9 WEEK/springmvc/src/main/java/hello/springmvc/SpringmvcApplication.java b/9 WEEK/springmvc/src/main/java/hello/springmvc/SpringmvcApplication.java new file mode 100644 index 00000000..d313cd0f --- /dev/null +++ b/9 WEEK/springmvc/src/main/java/hello/springmvc/SpringmvcApplication.java @@ -0,0 +1,13 @@ +package hello.springmvc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringmvcApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringmvcApplication.class, args); + } + +} diff --git a/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/HelloData.java b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/HelloData.java new file mode 100644 index 00000000..de6fa1f8 --- /dev/null +++ b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/HelloData.java @@ -0,0 +1,10 @@ +package hello.springmvc.basic; + + +import lombok.Data; + +@Data +public class HelloData { + private String username; + private int age; +} diff --git a/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/LogTestController.java b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/LogTestController.java new file mode 100644 index 00000000..5448e66d --- /dev/null +++ b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/LogTestController.java @@ -0,0 +1,29 @@ +package hello.springmvc.basic; + +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +public class LogTestController { +// private final Logger log = LoggerFactory.getLogger(getClass()); + + @RequestMapping("/log-test") + public String logTest() { + String name = "Spring"; + + System.out.println("name = " + name); + + log.trace("trace log={}", name); + log.debug("debug log={}", name); + log.info("info log={}", name); + log.warn("warn log={}", name); + log.error("error log={}", name); + + + return "ok"; + } +} diff --git a/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestBodyJsonController.java b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestBodyJsonController.java new file mode 100644 index 00000000..1d8d72c8 --- /dev/null +++ b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestBodyJsonController.java @@ -0,0 +1,68 @@ +package hello.springmvc.basic.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import hello.springmvc.basic.HelloData; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.stereotype.Controller; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@Slf4j +@Controller +public class RequestBodyJsonController { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @PostMapping("/request-body-json-v1") + public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + log.info("messageBody={}", messageBody); + HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); + log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); + + response.getWriter().write("ok"); + } + + @ResponseBody + @PostMapping("/request-body-json-v2") + public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException { + log.info("messageBody={}", messageBody); + HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); + log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); + return "ok"; + } + + @ResponseBody + @PostMapping("/request-body-json-v3") + public String requestBodyJsonV3(@RequestBody HelloData helloData) { + log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); + return "ok"; + } + + @ResponseBody + @PostMapping("/request-body-json-v4") + public String requestBodyJsonV4(HttpEntity httpEntity) { + HelloData data = httpEntity.getBody(); + log.info("username={}, age={}", data.getUsername(), data.getAge()); + return "ok"; + } + + @ResponseBody + @PostMapping("/request-body-json-v5") + public HelloData requestBodyJsonV5(@RequestBody HelloData data) { + log.info("username={}, age={}", data.getUsername(), data.getAge()); + return data; + } + + +} diff --git a/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestBodyStringController.java b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestBodyStringController.java new file mode 100644 index 00000000..fbbb77b0 --- /dev/null +++ b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestBodyStringController.java @@ -0,0 +1,53 @@ +package hello.springmvc.basic.request; + +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.stereotype.Controller; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.nio.charset.StandardCharsets; + +@Slf4j +@Controller +public class RequestBodyStringController { + + @PostMapping("/request-body-string-v1") + public void requestBodyStringV1(HttpServletRequest request, HttpServletResponse response) throws IOException { + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + + log.info("messageBody={}", messageBody); + response.getWriter().write("ok"); + } + + @PostMapping("/request-body-string-v2") + public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException { + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + log.info("messageBody={}", messageBody); + responseWriter.write("ok"); + } + + @PostMapping("/request-body-string-v3") + public HttpEntity requestBodyStringV3(HttpEntity httpEntity) throws IOException { + String messageBody = httpEntity.getBody(); + log.info("messageBody={}", messageBody); + return new HttpEntity<>("ok"); + } + + @ResponseBody + @PostMapping("/request-body-string-v4") + public String requestBodyStringV4(@RequestBody String messageBody) { + log.info("messageBody={}", messageBody); + return "ok"; + } + +} diff --git a/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestHeaderController.java b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestHeaderController.java new file mode 100644 index 00000000..55bc8db4 --- /dev/null +++ b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestHeaderController.java @@ -0,0 +1,39 @@ +package hello.springmvc.basic.request; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Locale; + +@Slf4j +@RestController +public class RequestHeaderController { + + @RequestMapping("/headers") + public String headers( + HttpServletRequest request, + HttpServletResponse response, + HttpMethod httpMethod, + Locale locale, + @RequestHeader MultiValueMap headerMap, + @RequestHeader("host") String host, + @CookieValue(value = "myCookie", required = false) String cookie + ) { + log.info("request={}", request); + log.info("response={}", response); + log.info("httpMethod={}", httpMethod); + log.info("locale={}", locale); + log.info("headerMap={}", headerMap); + log.info("header host={}", host); + log.info("myCookie={}", cookie); + return "ok"; + } + +} diff --git a/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestParamController.java b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestParamController.java new file mode 100644 index 00000000..3840880d --- /dev/null +++ b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/request/RequestParamController.java @@ -0,0 +1,98 @@ +package hello.springmvc.basic.request; + +import hello.springmvc.basic.HelloData; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.io.IOException; +import java.util.Map; + +@Slf4j +@Controller +public class RequestParamController { + + @RequestMapping("/request-param-v1") + public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException { + String username = request.getParameter("username"); + int age = Integer.parseInt(request.getParameter("age")); + log.info("username={}, age={}", username, age); + + response.getWriter().write("OK"); + + } + + @ResponseBody + @RequestMapping("/request-param-v2") + public String requestParamV2( + @RequestParam("username") String memberName, + @RequestParam("age") int memberAge) { + log.info("username={}, age={}", memberName, memberAge); + return "ok"; + } + + @ResponseBody + @RequestMapping("/request-param-v3") + public String requestParamV3( + @RequestParam String username, + @RequestParam int age) { + log.info("username={}, age={}", username, age); + return "ok"; + } + + @ResponseBody + @RequestMapping("/request-param-v4") + public String requestParamV4(String username, int age) { + log.info("username={}, age={}", username, age); + return "ok"; + } + + @ResponseBody + @RequestMapping("/request-param-required") + public String requestParamV5( + @RequestParam(required = true) String username, + @RequestParam(required = false) Integer age //int는 null이 못들어감 다만 Integer은 객체라 가능 + ) { + log.info("username={}, age={}", username, age); + return "ok"; + } + + @ResponseBody + @RequestMapping("/request-param-default") + public String requestParamDefault( + @RequestParam(required = true, defaultValue = "guest") String username, + @RequestParam(required = false , defaultValue = "-1") Integer age + ) { + log.info("username={}, age={}", username, age); + return "ok"; + } + + @ResponseBody + @RequestMapping("/request-param-map") + public String requestParamMap(@RequestParam Map paramMap) { + log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age")); + return "ok"; + } + + @ResponseBody + @RequestMapping("/model-attribute-v1") + public String modelAttributeV1(@ModelAttribute HelloData helloData) { + log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); + log.info("helloData={}", helloData); + return "ok"; + } + + + @ResponseBody + @RequestMapping("/model-attribute-v2") + public String modelAttributeV2( HelloData helloData) { + log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); + log.info("helloData={}", helloData); + return "ok"; + } +} diff --git a/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/requestmapping/MappingClassController.java b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/requestmapping/MappingClassController.java new file mode 100644 index 00000000..6de00cb1 --- /dev/null +++ b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/requestmapping/MappingClassController.java @@ -0,0 +1,35 @@ +package hello.springmvc.basic.requestmapping; + +import org.springframework.web.bind.annotation.*; + +@RequestMapping("/mapping/users") +@RestController +public class MappingClassController { + + + @GetMapping + public String user() { + return "get users"; + } + + @PostMapping + public String addUser() { + return "post user"; + } + + @GetMapping("/{userId}") + public String findUser(@PathVariable("userId") String userId) { + return "get userId=" + userId; + } + + @PatchMapping("/{userId}") + public String updateUser(@PathVariable("userId") String userId) { + return "update userId=" + userId; + } + + @DeleteMapping("/{userId}") + public String deleteUser(@PathVariable("userId") String userId) { + return "delete userId=" + userId; + } + +} diff --git a/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/requestmapping/MappingController.java b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/requestmapping/MappingController.java new file mode 100644 index 00000000..e31bab8c --- /dev/null +++ b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/requestmapping/MappingController.java @@ -0,0 +1,61 @@ +package hello.springmvc.basic.requestmapping; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +public class MappingController { + + @RequestMapping("/hello-basic") + public String helloBasic() { + log.info("helloBasic"); + return "ok"; + } + + +// @RequestMapping(value = "/mapping-get-v2", method = RequestMethod.GET) + @GetMapping("/mapping-get-v2") + public String mappingGetV2() { + log.info("mapping-get-v2"); + return "ok"; + } + + + @GetMapping("/mapping/{userId}") + public String mappingPath(@PathVariable("userId") String data) { + log.info("mappingPath userId={}", data); + return "ok"; + } + + @GetMapping("/mapping/users/{userId}/orders/{orderId}") + public String mappingPath(@PathVariable String userId, @PathVariable String orderId) { + log.info("mappingPath userId={}, orderId={}", userId, orderId); + return "ok"; + } + + @GetMapping(value = "/mapping-param", params = "mode=debug") + public String mappingParam() { + log.info("mappingParam"); + return "ok"; + } + + @GetMapping(value = "/mapping-header", headers = "mode=debug") + public String mappingHeader() { + log.info("mappingHeader"); + return "ok"; + } + + @PostMapping(value = "/mapping-consume", consumes = "application/json") + public String mappingConsume() { + log.info("mappingConsume"); + return "ok"; + } + + @PostMapping(value = "/mapping-produce", produces = "text/html") + public String mappingProduce() { + log.info("mappingProduce"); + return "ok"; + } + +} diff --git a/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/response/ResponseBodyController.java b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/response/ResponseBodyController.java new file mode 100644 index 00000000..4c4e1991 --- /dev/null +++ b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/response/ResponseBodyController.java @@ -0,0 +1,51 @@ +package hello.springmvc.basic.response; + +import hello.springmvc.basic.HelloData; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; + +@Slf4j +//@RestController +@Controller +public class ResponseBodyController { + @GetMapping("/response-body-string-v1") + public void responseBodyV1(HttpServletResponse response) throws IOException + { + response.getWriter().write("ok"); + } + + @GetMapping("/response-body-string-v2") + public ResponseEntity responseBodyV2() { + return new ResponseEntity<>("ok", HttpStatus.OK); + } + @ResponseBody + @GetMapping("/response-body-string-v3") + public String responseBodyV3() { + return "ok"; + } + @GetMapping("/response-body-json-v1") + public ResponseEntity responseBodyJsonV1() { + HelloData helloData = new HelloData(); + helloData.setUsername("userA"); + helloData.setAge(20); + return new ResponseEntity<>(helloData, HttpStatus.OK); + } + @ResponseStatus(HttpStatus.OK) + @ResponseBody + @GetMapping("/response-body-json-v2") + public HelloData responseBodyJsonV2() { + HelloData helloData = new HelloData(); + helloData.setUsername("userA"); + helloData.setAge(20); + return helloData; + } +} diff --git a/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/response/ResponseViewController.java b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/response/ResponseViewController.java new file mode 100644 index 00000000..fa46db92 --- /dev/null +++ b/9 WEEK/springmvc/src/main/java/hello/springmvc/basic/response/ResponseViewController.java @@ -0,0 +1,28 @@ +package hello.springmvc.basic.response; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +@Controller +public class ResponseViewController { + + @RequestMapping("/response-view-v1") + public ModelAndView responseViewV1() { + ModelAndView mav = new ModelAndView("/response/hello") + .addObject("data", "hello"); + return mav; + } + + @RequestMapping("/response-view-v2") + public String responseViewV2(Model model) { + model.addAttribute("data", "hello"); + return "/response/hello"; + } + + @RequestMapping("/response/hello") + public void responseViewV3(Model model) { + model.addAttribute("data", "hello"); + } +} diff --git a/9 WEEK/springmvc/src/main/resources/application.properties b/9 WEEK/springmvc/src/main/resources/application.properties new file mode 100644 index 00000000..6974d2af --- /dev/null +++ b/9 WEEK/springmvc/src/main/resources/application.properties @@ -0,0 +1,5 @@ +#logging.level.hello.springmvc = trace + +##default? +#spring.thymeleaf.prefix=classpath:/templates/ +#spring.thymeleaf.suffix=.html \ No newline at end of file diff --git a/9 WEEK/springmvc/src/main/resources/static/basic/hello-form.html b/9 WEEK/springmvc/src/main/resources/static/basic/hello-form.html new file mode 100644 index 00000000..422949c7 --- /dev/null +++ b/9 WEEK/springmvc/src/main/resources/static/basic/hello-form.html @@ -0,0 +1,14 @@ + + + + + Title + + +
+ username: + age: + +
+ + \ No newline at end of file diff --git a/9 WEEK/springmvc/src/main/resources/static/index.html b/9 WEEK/springmvc/src/main/resources/static/index.html new file mode 100644 index 00000000..e90fadd4 --- /dev/null +++ b/9 WEEK/springmvc/src/main/resources/static/index.html @@ -0,0 +1,85 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/9 WEEK/springmvc/src/main/resources/templates/response/hello.html b/9 WEEK/springmvc/src/main/resources/templates/response/hello.html new file mode 100644 index 00000000..28817115 --- /dev/null +++ b/9 WEEK/springmvc/src/main/resources/templates/response/hello.html @@ -0,0 +1,10 @@ + + + + + Title + + +

empty

+ + \ No newline at end of file diff --git a/9 WEEK/springmvc/src/test/java/hello/springmvc/SpringmvcApplicationTests.java b/9 WEEK/springmvc/src/test/java/hello/springmvc/SpringmvcApplicationTests.java new file mode 100644 index 00000000..75e5e568 --- /dev/null +++ b/9 WEEK/springmvc/src/test/java/hello/springmvc/SpringmvcApplicationTests.java @@ -0,0 +1,13 @@ +package hello.springmvc; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SpringmvcApplicationTests { + + @Test + void contextLoads() { + } + +}