1. 의존 객체 자동주입이란?
- 스프링 설정 파일에서 의존 객체를 주입할 때 <constructor constructor-org> 또는<property> 태그로 의존대상 객체를 명시하지 않아도 스프링 컨테이너가 자동으로 필요한 의존대상 객체를 찾아서 의존대상 객체가 필요한 객체에 주입해주는 기능이다.
- 구현방법은@Autowired Autowired와 @Resource 어노테이션을 이용해서 쉽게 구현 할 수있다.
2. DI 자동 주입 설정 방법
3. 의존 객체 자동 주입 태그
1) @ Autowired (required = false) (to니까 타입을 기준으로 주입함)
- 타입을 기준으로 의존성을 주입, 같은 타입 빈이 두 개 이상 있을 경우 변수이름으로 빈을 찾음
- 주입하려고하는객체의타입이일치하는객체를자동으로주입한다
- Spring 아노테이션
2) @Qualifier
- 빈의 이름으로 의존성 주입
- 모호한 bean의 강제 연결
- @Autowired 와 같이 사용
- Spring 아노테이션
- 동일한 객체가 2개 이상인 경우 스프링 컨테이너는 자동주입 대상 객체를 판단하지 못해서 ExceptionException을 발생시킨다.
3) @Resource (re 니까 이름을 속성으로 한다)
- name을 속성을 이용하여 빈의 이름을 직접 지정
- 주입하려고 하는 객체의 이름이 일치하는 객체를 자동으로 주입한다
- JavaSE의 아노테이션 (JDK9 에는 포함 안되어 있음)
4) @Inject
- @Autowired 아노테이션을 사용하는 것과 같다
- JavaSE의 아노테이션
* 예제(Document, Printer, MainClass, autowired-context.xml)
package ex05;
public class Document {
public String data = "내용..";
}
package ex05;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
public class Printer {
/*
* @Autowired - 타입으로 빈을 주입 → 이름으로 찾아서 빈을 주입
* 생성자, 세터, 멤버 변수에도 사용이 가능함 (세터, 멤버 변수는 반드시 기본 생성자가 필요)
*
* @Resource - 이름으로 빈을 주입 → 타입으로 찾아서 빈을 주입
*
* @Qulifier - 빈 이름의 강제 연결
* 컨테이너에 동일한 객체가 여러개 있을 때 어느 객체를 주입할지 선택해주는 어노테이션
*/
//생성자 - 주입 명령어를 쓰면 autowired-context에 constructor 안만들어줘도 됨.
@Autowired
@Qualifier(value = "doc1")
private Document doc;
//기본 생성자
public Printer() {
}
//생성자
public Printer (Document doc) {
this.doc = doc;
}
//getter, setter
public Document getDoc() {
return doc;
}
public void setDoc(Document doc) {
this.doc = doc;
}
}
package ex05;
import org.springframework.context.support.GenericXmlApplicationContext;
public class MainClass {
public static void main(String[] args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext("autowired-context.xml");
Printer pt = ctx.getBean("printer", Printer.class);
System.out.println(pt.getDoc().data);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<!-- 자동 주입 명령 - context의 모듈의 특정 기능이 필요로 합니다. -->
<context:annotation-config />
<!-- doc1, doc2일때는 오류지만 doc라고 했을때는 내용이 나옴 -->
<!-- Qulifier(value = "doc1")라고 작성하면 찾아줌 -->
<bean id ="doc1" class="ex05.Document" />
<bean id ="doc2" class="ex05.Document" />
<bean id="printer" class="ex05.Printer"/>
</beans>
* 연습 문제(controller, serviceimpl, dao, mainclass, autowired-context)
package ex06;
import org.springframework.beans.factory.annotation.Autowired;
public class Controller {
//MVC2 방식의 클래스 모형입니다.
//1. Controller에서 new키워드를 사용하지 말고 "자동주입"을 이용해서 Service의 hello를 호출시켜주세요
//2. Service에서는 new키워드를 사용하지 말고 "자동주입"을 이용해서 DAO의 hello를 호출시켜주세요
//3. DAO에 있는 리턴 값을 Controller로 반환받고 출력해주세요
//4. main에서는 컨트롤러 객체를 확인
//객체를 주입해준다.
@Autowired
private ServiceImpl si;
//생성자
// public Controller(ServiceImpl si) {
// this.si = si;
// }
//controller-service-dao가 연결 되어있으니까 컨트롤러에서 dao값도 연결되어 있다는 뜻0
public void hello() {
String result = si.hello();
System.out.println(result);
}
}
package ex06;
import org.springframework.beans.factory.annotation.Autowired;
public class ServiceImpl {
@Autowired
private DAO dao;
public String hello() {
return dao.hello();
}
}
package ex06;
public class DAO {
public String hello() {
return "hello";
}
}
package ex06;
import org.springframework.context.support.GenericXmlApplicationContext;
public class MainClass {
public static void main(String[] args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext("autowired-context.xml");
Controller con = ctx.getBean("controller", Controller.class);
con.hello();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<!-- 자동 주입 명령 - context의 모듈의 특정 기능이 필요로 합니다. -->
<context:annotation-config />
<!-- doc1, doc2일때는 오류지만 doc라고 했을때는 내용이 나옴 -->
<!-- Qulifier(value = "doc1")라고 작성하면 찾아줌 -->
<bean id ="doc1" class="ex05.Document" />
<bean id ="doc2" class="ex05.Document" />
<bean id="printer" class="ex05.Printer"/>
<!-- ex06 -->
<bean id = "si" class="ex06.ServiceImpl" />
<bean id = "dao" class="ex06.DAO" />
<bean id = "controller" class ="ex06.Controller" />
</beans>
4. xml 파일을 java 파일로 변경하기
- @Configuration: 스프링 컨테이너를 대신 생성하는 어노테이션(빈 설정 파일을
- @Bean: 빈으로 등록하는 어노테이션
- spring에서는 .xml로 하고 스프링 부트에서는 .java로 함(바뀌어도 상관 없음)
* 예제(javaconfig, application-context = xml을 java 파일로)
package ex07;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ex02.Hotel;
import ex02.MemberDAO;
import ex02.Chef;
import ex02.DatabaseDev;
//xml을 대신해서 설정 파일로 사용할 때
//이거 자바 설정 파일이야 할때 어노테이션 붙여줄 것
@Configuration
public class JavaConfig {
//bean이 붙은 메서드를 스프링 컨테이너가 호출시킴(빈으로 생성)
@Bean
public Chef chef() {
return new Chef();
}
@Bean
public Hotel hotel() {
return new Hotel(chef());
}
@Bean
public DatabaseDev dev() {
DatabaseDev dev = new DatabaseDev();
dev.setUrl("주소..");
dev.setUid("아이디..");
dev.setUpw("비밀번호...");
return dev;
}
@Bean
public MemberDAO dao() {
MemberDAO dao = new MemberDAO();
dao.setDatabaseDev(dev());
return dao;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 스프링 빈 설정 파일-->
<bean id = "test" class ="ex01.SpringTest" scope="prototype"/>
<!-- 생성자를 통한 주입, hotel은 chef에 의존함 -->
<bean id="chef" class="ex02.Chef" />
<bean id="hotel" class="ex02.Hotel" >
<constructor-arg ref="chef"/>
</bean>
<!-- 세터를 통한 주입 -->
<bean id="dev" class="ex02.DatabaseDev">
<!-- DatabaseDev는 문자열, 비어 있으므로 property로 넣어줌 -->
<property name="url" value="데이터베이스 주소" />
<property name="uid" value="데이터베이스 계정명" />
<property name="upw" value="데이터베이스 비밀번호" />
</bean>
<bean id="dao" class="ex02.MemberDAO">
<property name="databaseDev" ref="dev"/>
</bean>
</beans>
* mainclass에서 예제 결과 보기
package ex07;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import ex02.MemberDAO;
public class MainClass {
public static void main(String[] args) {
//자바 설정으로 만들어진 bean 파일은
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(JavaConfig.class);
//정의 되어 있는 빈의 갯수
//내부적으로 동작하는 빈의 갯수도 포함한다
// System.out.println(ctx.getBeanDefinitionCount());
//헷갈리는 부분
//getBean에서 첫번째 "dao"는 xml 설정파일 <bean>태그의 id 속성값을 적어주면 된다. = 구하고자 하는 스프링 빈 객체의 이름
//두번째 파라미터는 그 빈 객체의 타입이다.
MemberDAO dao = ctx.getBean("dao", MemberDAO.class);
System.out.println(dao.getDatabaseDev().getUid());
System.out.println(dao.getDatabaseDev().getUpw());
System.out.println(dao.getDatabaseDev().getUrl());
}
}
5. 웹 프로그래밍을 구축하기 위한 설계 모델(중요)
- 스프링 MVC는 Model2 방식을 따르며 이 Model2의 아키텍처에 맞게 설계되어 있다.
1) 클라이언트에서 요청이 들어오면 이 요청(ex. 회원정보, 글..) 이 frontcontrol이라고 하는 dispatcherservlet(화면 앞단에서 모든 요청을 잡아서 가져옴)로 요청(모든걸 다 제어하는게 dispatcherservlet임)
2) 이걸 handlerMapping : url 주소를 분석해서 url이 어떤식으로 만들어져 있는지 분석해서 다시 dispatcherservlet으로 반환
3) 다시 결과를 dispatcherservlet이 분기된 url주소를 반환 받아서 적합한 클래스를 붙여주기 위해서 handlerAdapter로 갔다가 특정 controller(우리가 만드는 것)-<뒤에는 서비스랑, dao가 있음>로 연결을 붙여줌
4) 데이터를 들고 다시 컨트롤러로 나옴
5) 컨트롤러에서 정보 리턴: 컨트롤러에서는 모델(data)이라고 불리는 데이터 정보, view라고 불리는 화면 정보를 다시 dispatcherservlet으로 보냄
6) dispatcherservlet은 데이터, 화면 정보를 받아서 viewResolver(뷰에 대한 정보)로 보내줌(뷰 경로를 합성해서)
7) viewResolver은 완성된 뷰에 대한 정보를 다시 dispatcherservlet으로 반환
8) dispatcherservlet : view 경로 정보, 데이터 정보 다 가지고 있음
9) dispatcherservlet은 이제 forward방식(데이터를 가지고 화면으로 나간다)으로 화면으로 넘겨준다.
< 정리>
|
* spring doc 참고
6. JSP에 스프링 조립하기
- 필요한 스프링 코드를 순서대로 추가합니다
1) 자바 버전 스프링 버전 변수 선언
2) dependencies 태그 선언
3) 스프링코어 다운
4) 스프링 webMVC 다운
5) web.xml 에 스프링 servlet 설정 프로젝트 최초 가동시 동작
6) servlet.xml 서블릿 설정
- 기존 프로젝트 오른쪽 클릭: close project
- 이제 여기다가 maven 붙이기
- 그 다음에 여기에 들어가서 spring 검색
* pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.simple</groupId>
<artifactId>SpringMake</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<!-- 변수 선언: 핸들러를 구현해보기 위해서 3버전을 활용합니다. (5버전에서는 사라짐) -->
<properties>
<org.springframework-version>
3.1.1.RELEASE
</org.springframework-version>
</properties>
<!-- 필요한 라이브러리들을 다운로드 -->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.3</version>
</plugin>
</plugins>
</build>
</project>
- 그 다음에 꼭! 프로젝트 우클릭 - maven - project update
- 그 다음 web.xml에 코드 블럭처럼 수정
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
id="WebApp_ID" version="4.0">
<display-name>SpringMake</display-name>
<!-- 전역으로 사용할 초기 설정 파일 -->
<!-- 모든 서블릿에서 공통으로 참조할 파일은 전역으로 선언할 수 있습니다. -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/config/database-servlet.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 서블릿 등록 -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 디스패처 서블릿이 생성될 때 스프링 설정 파일로 사용할 파일의 경로를 매개변수로 받습니다. -->
<!-- 이 파일이 혹 여러개라면, 줄바꿈으로 여러개 표현해주면 됩니다. -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
<!-- 디스패처 서블릿: 초기 설정 파일 -->
/WEB-INF/config/spring-servlet.xml
</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
* 오류
- 마지막 사진처럼 오류나서 구글링 해보니까 밑에와 같은 원인들이 존재한다고 한다.
- 선생님과 함께 작성한 파일을 비교하니까 파일 크기가 차이가 나긴 한데 어떤 부분이 다른지는 잘 모르겠다.
web-xml의 엘리먼트 설정 쪽이 잘못됨 xml 파일의 내용이 없는 것 (파일 크기가 0인 것) xml 파일 자체가 없는 경우 인코딩 관련 문제인 경우 xml파일 내에 닫힘 태그가 없는 경우 파싱 대상인 파일의 형식이 xml이 아닌 경우 |
* MainController
package com.simple.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
//핸들러 클래스로 등록이 되려면 특정 기능을 가지고 있어야 합니다.
//MultiActionController 스프링 3 버전에서 컨트롤러의 역할을 할 수 있도록 제공되는 클래스 중 하나입니다.
//상속을 받고 handleRequestInternal을 오버라이딩 하면 동작합니다.
public class MainController extends MultiActionController{
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
System.out.println("실행됨");
return super.handleRequestInternal(request, response);
}
}
* spring-servlet
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 디스패처 서블릿이 초기로 등록하는 설정 파일 -->
<!-- 메인 컨트롤러를 빈으로 등록 -->
<bean id = "xxx" class="com.simple.controller.MainController"/>
<!-- 핸들러 맵핑 등록 -->
<!-- /test/aaa 요청이 들어오면 xxx bean으로 핸들러 맵핑을 시킨다. -->
<bean class ="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name = "mappings">
<props>
<!-- key에는 url주소가 들어감 -->
<prop key="/test/aaa">xxx</prop>
</props>
</property>
</bean>
</beans>
- window에서 web-browser: chrome으로 변경하고 볼 것
- 주소창에 /test/aaa라고 치면
* MainController 구문 수정
package com.simple.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
//핸들러 클래스로 등록이 되려면 특정 기능을 가지고 있어야 합니다.
//MultiActionController 스프링 3 버전에서 컨트롤러의 역할을 할 수 있도록 제공되는 클래스 중 하나입니다.
//상속을 받고 handleRequestInternal을 오버라이딩 하면 동작합니다.
public class MainController extends MultiActionController{
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
String path = request.getContextPath();
String uri = request.getRequestURI();
String command = uri.substring( path.length() );
if(command.equals("/test/aaa")) {
System.out.println("aaa 실행");
} else if(command.equals("/test/bbb")) {
System.out.println("bbb 실행");
}
return super.handleRequestInternal(request, response);
}
}
* MainController 확장
package com.simple.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
//핸들러 클래스로 등록이 되려면 특정 기능을 가지고 있어야 합니다.
//MultiActionController 스프링 3 버전에서 컨트롤러의 역할을 할 수 있도록 제공되는 클래스 중 하나입니다.
//상속을 받고 handleRequestInternal을 오버라이딩 하면 동작합니다.
public class MainController extends MultiActionController{
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
String path = request.getContextPath();
String uri = request.getRequestURI();
System.out.println(path); // /SpringMake
System.out.println(uri); // /SpringMake/test/aaa
System.out.println(path.length()); //11
//즉 여기서 uri값에서 11만큼 substring하면 /test/aaa만 남음
String command = uri.substring( path.length() );
if(command.equals("/test/aaa")) {
System.out.println("aaa 실행");
//확장...
//model과 view 정보를 담는 객체
ModelAndView mv = new ModelAndView();
// mv.setViewName("/WEB-INF/views/home.jsp"); //뷰의 정보
mv.setViewName("home");
mv.addObject("data", "hello world!"); //데이터 정보
return mv;
} else if(command.equals("/test/bbb")) {
System.out.println("bbb 실행");
}
return super.handleRequestInternal(request, response);
}
}
* home.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
hello world!
</body>
</html>
- 근데 위에 있는 메인 컨트롤러에서 mv.setViewName("/WEB-INF/views/home.jsp"); 이렇게 setViewName에다가 주소 전체를 매번 써주기는 너무 번거로울 수 있어 뷰 합성기를 추가하면 간편하게 home만 입력하더라도 view를 볼 수 있다.
* Spring-servlet(뷰 합성기 추가)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 디스패처 서블릿이 초기로 등록하는 설정 파일 -->
<!-- 메인 컨트롤러를 빈으로 등록 -->
<bean id="xxx" class="com.simple.controller.MainController" />
<!-- 핸들러 맵핑 등록 -->
<!-- /test/aaa 요청이 들어오면 xxx bean으로 핸들러 맵핑을 시킨다. -->
<bean
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<!-- key에는 url 주소가 들어감 -->
<prop key="/test/aaa">xxx</prop>
<prop key="/test/bbb">xxx</prop>
</props>
</property>
</bean>
<!-- 뷰 합성기(뷰리졸버) -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name = "prefix" value="/WEB-INF/views/" />
<property name = "suffix" value=".jsp" />
</bean>
</beans>
package com.simple.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
//핸들러 클래스로 등록이 되려면 특정 기능을 가지고 있어야 합니다.
//MultiActionController 스프링 3 버전에서 컨트롤러의 역할을 할 수 있도록 제공되는 클래스 중 하나입니다.
//상속을 받고 handleRequestInternal을 오버라이딩 하면 동작합니다.
public class MainController extends MultiActionController{
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
String path = request.getContextPath();
String uri = request.getRequestURI();
String command = uri.substring( path.length() );
if(command.equals("/test/aaa")) {
System.out.println("aaa 실행");
//확장...
//model과 view 정보를 담는 객체
ModelAndView mv = new ModelAndView();
// mv.setViewName("/WEB-INF/views/home.jsp"); //뷰의 정보
mv.setViewName("home");
mv.addObject("data", "hello world!"); //데이터 정보
return mv;
} else if(command.equals("/test/bbb")) {
System.out.println("bbb 실행");
}
return super.handleRequestInternal(request, response);
}
}
- prefix에 앞 주소 경로, suffix에는 뒷 주소 경로를 붙여서 ~.jsp에서 물결 : setViewName("home") 이렇게만 해줘도
/test/aaa를 입력했을 때 home.jsp를 볼 수 있다.
- 즉 위 코드 블럭에서처럼 mv.setViewName("home"); 추가하더라도 같은 결과가 나와야 함.
* 오류
- 테스트를 반복하며 생긴 오류: /test/aaa를 쳤을 때 home.jsp가 안나왔다.
- 뷰 합성기에 있는 value값과 폴더 이름이 같아야 한다는 점을 알 수 있었다.
'TIL > Spring' 카테고리의 다른 글
day78-spring (0) | 2023.02.06 |
---|---|
day77-spring (0) | 2023.02.03 |
day76-spring (0) | 2023.02.02 |
day75-spring (1) | 2023.02.01 |
day73-spring (0) | 2023.01.30 |