dev-course day19

2rlokr·2025년 3월 28일

dev-course

목록 보기
19/43
post-thumbnail

오늘 배운 것

실습

Spring Proxy

  • Spring Proxy는 프록시 대상 객체가 인터페이스를 구현한 구현 클래스라면 JDK 동적 프록시를 사용한다.
static class SubLogicInterceptor implements MethodInterceptor {

	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {

		log.info("[INTERCEPTOR] 전처리!");
        Object result = invocation.proceed();
        log.info("[INTERCEPTOR] 후처리!");

        return result;
	}
}

ProxyFactory case 1 : JDK 동적 프록시

@Test
@DisplayName("스프링 프록시 팩토리에서는 프록시 대상 객체가 인터페이스를 구현한 구체 클래스라면, JDK를 사용한다.")
void proxy_test_1() throws Exception {

	MockService target = new MockServiceImpl();

	// 인터페이스가 있나? -> 있다 -> JDK 써야지 ~
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.addAdvice(new SubLogicInterceptor());

    MockService proxy = (MockService) proxyFactory.getProxy();

    log.info("target.getClass() = {}", target.getClass());
    log.info("proxy.getClass() = {}", proxy.getClass());

    proxy.logic();
}
  • ProxyFactory 라는 클래스의 객체를 생성하고, 대상 객체인 target을 넣어준다.
  • 여기서, targetMockService라는 인터페이스 타입의 인터페이스 구현 클래스 생성자를 호출한 인스턴스이다. 즉, 인터페이스를 기반이라는 것!
  • addAdvice를 통해 관심사의 코드를 추가해주고, getProxy()로 프록시를 만들어준다.
  • logic() 메서드 호출 시 invoke()가 호출된다.
  • proxyMockService로 캐스팅했지만, .getClass()를 했을 때 class jdk.proxy3.$Proxy13가 나온다.

case 2 : CGLIB 방식

@Test
@DisplayName("구상 클래스(Concrete Class)라면, 스프링 프록시 팩토리는 CGLIB 방식으로 프록시 객체를 생성한다.")
void proxy_test_2() throws Exception {

	ConcreteMockService target = new ConcreteMockService();

    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.addAdvice(new SubLogicInterceptor());

    ConcreteMockService proxy = (ConcreteMockService) proxyFactory.getProxy();

    log.info("target.getClass() = {}", target.getClass());
    log.info("proxy.getClass() = {}", proxy.getClass());

    proxy.logic1();

}
  • ConcreteMockService는 구현 클래스이다.
  • proxy.getClass()를 호출했을 때 ConcreteMockService$$SpringCGLIB$$0 이러한 결과가 나오는 것을 알 수 있다.
  • 구현 클래스 기반 프록시기 때문에 CGLIB 방식이 사용되었다.

case 3

proxyFactory.setProxyTargetClass(true); // 대상 클래스를 상속받는 방식으로 CGLIB 방식으로 만들어준다.
  • 다음과 같이 설정해주었을 때는, 대상 클래스를 상속 받는 방식인 CGLIB 방식으로 만들어준다.

Aspect

@Slf4j
@Aspect
@Component
public class LoggingAspect {

    // execution(접근제어자 반환타입 메서드이름(파라미터) [예외])
	// @Around("execution(* * io.silver.greppaop.app.AopService.logic(String, String))")
    // @Around("@annotation(io.silver.greppaop.config.Logging)")
    @Around("execution(public void io.silver.greppaop.app.AopService.logic())")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        log.info("[LoggingAspect] 횡단 관심사 로깅 시작");
        Object result = joinPoint.proceed();
        log.info("[LoggingAspect] 횡단 관심사 로깅 종료");

        return result;
    }

}
  • Springboot에서는 관심사를 @Aspect로 등록해줄 수 있다.

@Around
실행 전, 후에 Advice를 실행하겠다는 것이다.
ProceedingJoinPoint를 인자로 받을 수 있다는 것이 큰 특징이다.
Object를 반환해준다.

execution
point cut을 통해 필터링을 해줄 수 있는데, 그 중 하나가 execution이다.
execution(접근제어자 반환타입 메서드이름(파라미터) [예외]) 로 어디에 적용될지 필터링해줄 수 있다.


Spring Boot

1번 예제

// http://localhost:8080/hello-world
@RequestMapping(method = RequestMethod.GET, path = "/hello-world")
public void receiveRequestFromBrowser() {
	log.info("Hello World!");
}
  • 원래 end-point를 맵핑해주는 어노테이션은 @RequestMapping 라고 한다.
  • RequestMethod라는 enum에 Http 메서드가 담겨있다.

2번 예제

@GetMapping("/print-spring")
public void printSpring1(HttpServletResponse resp) throws Exception{
    resp.setContentType("text/html;charset=UTF-8");
    PrintWriter writer = resp.getWriter();
    writer.println("안녕하세요!");
}
  • HttpServletResponse 를 받아올 수 있다.
  • text/html;charset=UTF-8 세팅을 해두어야 응답 본문에 한국어가 담겨있어도 잘 보일 수 있다.
  • setContentType를 통해 먼저 세팅한 후 그걸로 writer를 불러와야 오류가 안 난다. (순서 지켜야 한다)
  • ControllerView를 반환하는데, 여기서는 응답 본문에 값이 담겨있기 때문에 View가 필요없고, 오류가 나지 않는다.

3번 예제

@ResponseBody // 
@GetMapping("/print-spring3")
public String printSpring3() {
	return "안녕하세요! 반가워요!"; // 반환하는 문자열을 바디에 넣어준다.
}
  • @ResponseBody를 이용하면, 더이상 view를 찾지 않고, 인코딩도 자동으로 해준다.
  • HttpMessageConverter의 구현체 (스프링이 만드는)가 본문 응답을 반환값을 가지고 만들어준다.
    • StringHttpMessageConverter : 반환 타입이 문자열일 때
    • MappingJackson2HttpMessageConverter 문자열이 아닌 객체일 때
  • Content type을 확인해보면 다음과 같이 나온다. Content-Type: text/html;charset=UTF-8

4번 예제

@ResponseBody // 더이상 view를 찾지 않는다. 인코딩도 해준다.
@GetMapping("/print-integer") // "/print-spring3"를 end-point라고 부른다.
public Integer printInteger() {
	return 1;
}
  • 1을 반환한 경우, Content type을 확인해보면 Content-Type: application/json 이라고 나온다.
  • 내부적으로 Spring의 HttpMessageConverter가 반환값을 적절한 형식으로 변환하여 클라이언트에게 응답한다.
  • 이 예제에서는 Integer 타입을 반환하므로, MappingJackson2HttpMessageConverter가 사용되어 JSON 형식(application/json)으로 변환된다.

5번 예제

@Getter
class SomeType {
	private final String data = "데이터!!";
}

@ResponseBody
@GetMapping("/print-obj")
public SomeType printObj() {
	return new SomeType();
}
{
    "data": "데이터!!"
}
  • 다음과 같이 실행결과가 나온다.
  • 반환값이 객체일 경우, json 형태로 전달된다.
  • 뷰가 아닌 데이터를 통신하는 것을 API 통신이라고 한다.
  • 반환 값에 html을 넣을 경우, 렌더링이 되어 보인다. view가 없어도 html을 보낼 수 있다.

6번 예제

@GetMapping("/page-2")
    public String showPage2() {
    // /templates/txt_page.txt
    return "txt_page"; // 
}

꼭 view는 html파일이어야 할까? -> 그렇지 않다 !

  • application.yml 혹은 application.properties에 설정을 해둘 수 있다.
# application.yml 설정법
spring:
  thymeleaf:
#    prefix : classpath:/custom/
    suffix : .txt
  • YAML 형식
  • YAML은 계층 구조 표현이 가능해서 복잡한 설정을 다룰 때 더 직관적이다.
# application.properties 설정법
  spring.thymeleaf.prefix = classpath:/templates/
  spring.thymeleaf.suffix = .txt
  • application.properties는 텍스트 형식이고, 키 = 값 형태로 나타낸다.

7번 예제

@GetMapping("/params1")
public String showParams1(HttpServletRequest req) {

	String name = req.getParameter("name");
	log.info("name = {}", name);

	Map<String, String[]> parameterMap = req.getParameterMap();

	// parameter 얻는 방법 1번
	Set<Entry<String, String[]>> entries = parameterMap.entrySet();// key value가 묶여서 셋으로 나옴

	// parameter 얻는 방법 2번
	for (Map.Entry<String, String[]> entry : entries) {
		log.info("{} = {}", entry.getKey(), Arrays.toString(entry.getValue()));
	}

	// parameter 얻는 방법 3번
	parameterMap.forEach((key, value) -> log.info("key = {}, value = {}", key, value));

	return "index";
}
  • HttpServletRequest 를 이용하여 파라미터를 가져올 수 있다.
  • index.html 파일이 있을 경우, 경로를 지정해주지 않은 localhost:8080index 뷰가 자동으로 뜬다.

8번 예제

@GetMapping("/params3")
public String showParams3(@RequestParam(name = "name", required = false) String name
, @RequestParam(name = "age", required = false, defaultValue = "1") Integer age)
{
	// default value는 문자열로 줘야 한다.

	log.info("name = {}", name);
	log.info("age = {}", age);

	return "index";
}
  • @RequestParam을 이용해서 파라미터를 받아올 수 있다.
    • name = "name" 에서 앞에 name은 고정이고, 파라미터의 이름이 ""에 들어간다.
    • 받아온 파라미터를 이 메서드 안에서 뭐로 사용할 것이냐? -> String name 에 해당한다.
  • required=false를 설정해두면, 파라미터가 없을 때 에러가 뜨지 않는다.
  • defaultValue 기본 값을 설정해둘 수 있고, 문자열로 값을 넣어줘야 한다.

9번 예제

@GetMapping("/params4")
public String showParams4(String name){
	// 이렇게 해도 name으로 파라미터 하면 들어온다.
	// nickname=~~ 이렇게 하면 안 들어온다.

	log.info("name = {}", name);

	return "index";
}
  • RequestParam이 없더라도, name에 매칭이 된다면, 파라미터로 받아올 수 있다.
  • 단, 여기서 nickname=santa 라고 하면, name=null이 되는 것이다.

10번 예제

@Getter
@Setter
@ToString
@EqualsAndHashCode
@Data // getter, setter, toString, equalsAndHashCode 다 들어있다.
class SignInRequest {
	private String username;
	private String password;
}

@GetMapping("/params5")
public String showParams5(@ModelAttribute SignInRequest signInRequest){

	log.info("signInRequest = {}", signInRequest);

	log.info("signInRequest.getUsername() = {}", signInRequest.getUsername());
	log.info("signInRequest.getPassword() = {}", signInRequest.getPassword());

	return "index";
}
  • 객체가 mutable 해야만 값을 넣어줄 수 있다.
    • 즉, setter 나 생성자가 있어야한다.
  • @ModelAttribute 를 예전에는 필수로 붙여줘야 했었는데, 이젠 바껴서 생략 가능하다.
  • @Data 어노테이션을 붙여줄 경우, getter, setter, ToString, EqualsAndHashCode를 다 포함할 수 있다.

11번 예제

@GetMapping("/params7")
public String showParams7(String[] favorites){
	// List<String> 으로 보내면 안된다.

	for (String favorite : favorites) {
		log.info("favorite = {}", favorite);
	}

	return "index";
}
  • favorites라는 파라미터 여러 개를 받아오고 싶을 때는 String[] 배열로 받아올 수 있다. 단, List<String>는 객체이기 때문에 사용할 수 없다.

12번 예제

@GetMapping("/path1/{name}") // 경로 변수 만들 수 있다.
public String showPage1(@PathVariable(name="name") String name) {
	log.info("name = {}", name);
	return "index";
}

@GetMapping("/path1/admin") // 구체적인 게 우선이다.
public String showPage2() {
	log.info("ADMIN");
	return "index";
}
  • http://ej.blog/posts/1, http://ej.blog/posts/2,... 이러한 경로가 필요할 때, 경로에 맞는 메서드를 다 만들어주느냐? -> NO!
  • 경로 변수를 만들 수 있다. { } 안에 변수를 넣어줄 수 있다.
  • @PathVariable 을 사용하여 uri에서 변수를 받아올 수 있다.
    • name="name"에서 앞의 name은 고정이고, ""이 경로 변수가 되는 것이다.
    • String name이 이 메서드에서 사용할 변수
  • 위의 예제에서 localhost:8080/path1/admin을 실행시킬 때, 어떤 메서드가 실행될까?
    • 더 구체적인 showPage2가 실행된다.

13번 예제

@GetMapping({"/path2", "/path2/{name}"})
public String showPage3(@PathVariable(name="name", required = false) Optional<String> name) {
	// Optional<String> 을 넣어서 null도 처리할 수 있다.
	// required=false 여야 null이 들어갈 수 있다.
	log.info("name = {}", name);
	return "index";
}
  • required = false 를 설정해두면 null도 처리할 수 있다.
  • Optional<String>을 통해 null을 처리할 수 있다.
  • 이 경우, /path2/를 실행시키면 name = null 이 아니라, 에러가 난다.
    • 다른 경로로 인식한다.
    • 슬레시를 기준으로 자원의 계층을 인식한다.

14번 예제

@GetMapping("/boards/{boardName}/posts/{postId}")
public String showPage4(@PathVariable String boardName, @PathVariable String postId) {

	log.info("boardName = {}", boardName);
	log.info("postId = {}", postId);

	return "index";
}
  • 여러 개의 경로 변수도 얻어올 수 있다.
  • 또한, @PathVariable 파라미터를 주지 않아도, boardName, postId와 이름을 같게 사용하면 경로 변수를 잘 받아올 수 있다.
  • 단, 이 경우 비슷하게 더 구체적으로 구현한 메서드가 있다면, 그 메서드가 우선적으로 실행된다.

느낀 점

오늘 내용은 다행히 잘 따라갈 수 있었다 ! 오늘은 스프링에게 미움을 느끼지 않았다. ㅎㅎ 어제와 다른 그녀 스프링.. 암튼 다행이다. 오늘은 재밌게 들었던 것 같다 ! 그리고 너무 신기했다.. ㅎ 웹에 막 뭐가 뜨고,, 주소 바꾸니까 다른 거 뜨고.. 아 재밌어
(이렇게만 가주라 응응)

아~~~ Friday!!!! 금요일 ! Freitag !! 아, 행복하다. 근데 약속은 없다. 허허 집 최고.. 근데 좀 밖에 나가고 싶기도 해 이젠 ㅎ..

오늘 아침에 일어나서 밖을 봤는데 에..? 언제 벚꽃이 이렇게 폈어..? 진짜 신기하다.. 내가 모르는 사이에 열심히도 폈구나.. 예쁘다 팝콘 같아 ! 벚꽃 구경 가고 싶다 ~ 이번 주 주말도 알차게 잘 보내야지 ㅎㅎ 복습하고,, 스터디 하고 헤헤

다음주 금요일만 기다려요 ~ :)

0개의 댓글