spring 기초지식 학습정리

박서연·2023년 3월 20일
0

웹 스터디 기록

목록 보기
1/4
post-thumbnail

spring framework란?

: 자바 플랫폼을 위한 오픈소스 어플리케이션 프레임워크로 엔터프라이즈급 어플리케이션 개발을 위한 모든 기능을 종합적으로 제공하는 경량화된 솔루션.

  • 엔터프라이즈급 개발 환경: 대규모 데이터 처리와 트랜잭션이 동시에 여러 사용자로 부터 행해지는 매우 큰 규모의 환경.
  • 경량 컨테이너로 자바 객체를 담고 직접 관리.
  • 객체의 생성 및 소멸, 라이프사이클을 관리하며 언제든 spring 컨테이너로부터 필요한 객체를 가져와 사용 => spring은 IOC기반의 Framework임을 알 수 있음.
  • spring 이전에는 비즈니스 로직을 구현하기 위해 기술 자체에 대한 공부를 추가적으로 해야만 했었음. 비즈니스 로직을 구현하는 기술 자체가 복잡하고 어려웠음.
    -> 스프링 이후부터는 개발 초기에 기본적인 설정과 적용시킬 기술들만 잘 선택을 해준다면, 기술보다는 어플리케이션의 로직 자체에 더 집중하여 비즈니스 로직을 구현할 수 있음.
  • 웹 개발에서의 프레임워크 : 어떠한 목적을 쉽게 달성할 수 있도록 해당 목적과 관련된 코드의 뼈대를 미리 만들어 둔 것.
  • 어플리케이션 프레임워크 : 특정 업무 분야 및 특정 기술이 아닌, 어플리케이션 개발에 필요한 모든 과정에 집중하여 어플리케이션 개발을 하는데에 있어 필요한 모든 업무 분야 및 모든 기술과 관련된 코드들의 뼈대를 제공.
  • spring 특징 : POJO 프로그래밍을 지향
  • POJO(Plain Old Java Object, 즉 순수 Java만을 통해 생성한 객체를 의미)
  • 순수 Java만을 사용한다는 것은 Java 및 Java의 스펙에 정의된 기술만을 사용한다는 의미로, 어떤 객체가 외부의 라이브러리나 외부의 모듈을 가져와 사용중이라면, 그 객체는 POJO라고 할 수 없고, 다른 기술을 사용하지 않는 순수한 Java만을 사용하여 만든 객체.
  • POJO가 중요한 이유
    : 외부 라이브러리를 import하여 라이브러리의 메소드를 사용하고 있는 객체가 있다고 가정하면, 순수 Java 외의 기술을 사용하기 때문에 POJO가 아닌 상황. 이때 이 객체가 사용하는 기술이 변화가 생겨 기존 기술과 관련된 코드를 고쳐야 하는 상황이 발생하면 해당 기술을 사용하고 있는 모든 객체들의 코드를 전부 바꿔줘야 함. 해당 객체가 외부 모듈에 직접적으로 의존하기 때문에 발생하는 문제.
    -> 반면 POJO의 경우에는 특정 기술이나 환경에 종속되지 않고, 외부 기술이나 규약 변화에 얽매이지 않아 보다 유연한 변화와 확장을 대처할 수 있음.
    • 이러한 POJO를 사용해 비즈니스 로직을 구현하면 객체지향 설계를 제한없이 적용할 수 있고, 코드가 단순해져 테스트와 디버깅 또한 쉬워짐.
    이처럼 비즈니스 로직을 구현하는데 POJO 방식을 적극적으로 활용하는 프로그래밍 패러다임을 POJO 프로그래밍이라고 함.
  • 이러한 POJO 프로그래밍을 위해 스프링이 지원하는 기술에는 Ioc/DI, AOP, PSA가 있음.

0. spring framework의 구조

  • spring core : spring container를 의미.
    • continer는 spring framework의 핵심이며, 그 중 핵심은 Bean Factory Container임.
      • Bean Factory는 IOC패턴을 적용하여 객체 구성부터 의존성처리까지 모든 일을 처리하는 역할을 함.
  • spring Context
    : spring framework의 context 정보들을 제공하는 설정 파일.
    • spring context에는 JNDI, EJB, Validation, Scheduiling, Internaliztaion 등 엔터ㅡ라이즈 서비스들을 포함.
  • spring AOP
    : spring Framework에서 관점지향 프로그래밍을 할 수 있고, AOP를 적용할 수 있게 도와주는 Module임.
  • spring DAO
    : Data Access Object의 약자로 Database Data에 접근하는 객체. Spring JDBC DAO는 추상 레이어를 지원하며, 코딩이나 예외처리하는 부분을 간편화시켜 일관된 방법으로 코드를 짤 수 이쎅 도와줌.
  • spring ORM
    : Object relational mapping의 약자로, 간단히 객체와의 관계 설정을 하는 것. Spring에서는 Ibatis, Hibernate, JDO 등 인기있는 객체 관계형 도구를 사요할 수 있도록 지원.
  • spring Web
    : spring에서 Web context module은 Application module에 내장되어 있으며, Web기반의 응용프로그램에 대한 context를 제공하여 일반적인 Web Application 개발에 필요한 기본적인 기능을 지원.
  • spring MVC
    : Model2 구조로 Application을 만들 수 있도록 지원. MVC(Model-View-Controlller) 프레임워크는 웹 응용 프로그램을 작성하기 위한 완전한 기능을 갖춘 MVC를 구현함.

Ioc

1. Inversion of Control

: 의존성에 대한 컨트롤이 뒤바꼈다고?

  • 기존 사용자가 모든 작업을 제어하던 것을 특별한 객체에 모든 것을 위임하여 객체의 생성부터 생명주기 등 모든 객체에 대한 제어권이 넘어간 것
  • 기존 : 의존성에 대한 컨트롤은 자기 자신이 들고 있음.
class OwnerController{
	private OwnerRepository repository = new OwnerRepository();
}
  • spring : 나 외의 누군가가 밖에서 의존성을 넣어 줌.
class OwnerController{
	private OwnerRepository repo;
    
    public OwnerController(OwnerRepository repo){
    	this.repo = repo;
    }
    
    //이후에는 repo를 사용하는 코드 작성
}

class OwnerContorllerTest{
	@Test
	public void create(){
		OwnerRepository repo = new OwnerRepository();
    	OwnerController controller = new OwnerController(repo);
	}
}

누군가 생성자에서 생성자로 주겠지라는 마인드로 만들어짐.
OwnerContorleerTest에서 OwnerRepository를 만들어서, OwnerController한테 생성자를 통해 넘겨줌. 이것이 바로 의존성 주입. 이런 형태가 바로 Inversion of Control.

  • 일반적으로 지금까지의 프로그램은 객체 결정 및 생성 -> 의존성 객체 생성 -> 객채 내의 메소드 호출 작업을 반복함. 즉, 모든 작업을 사용자가 제어하는 구조.
  • 하지만 IOC에서의 객체는 자기가 사용할 객체를 선택하거나 생성하지 않음. 자신이 어디서 만들어지고 어떻게 사용되는지 모름. 자신의 모든 권한을 다른 대상에 위임하여 제어권한을 위임받은 특별한 객체에 의해 결정되고 만들어짐.
    즉, 제어의 흐름을 사용자가 컨트롤하지 않고 위임한 특별한 객체에 모든 것을 맡기게 됨.

2. IoC(Inversion of Control) 컨테이너

: spring은 Ioc용 컨테이너를 제공해 줌. 빈(bean)을 만들고 엮어주며 제공해주는 역할.

  • ApplicationContext(Beanfactory) : 컨테이너의 가장 핵심적인 인터페이스

    class OwnerController{
    	private OwnerRepository repo;
       
       public OwnerController(OwnerRepository repo){
       	this.repo = repo;
       }
       
       //이후에는 repo를 사용하는 코드 작성
    }

    이런 코드들을 동작하게 만들어 주는 역할. 실제 코드에 ApplicationContext라는 단어는 찾아볼 수 없음. 빈들의 의존성을 관리해 줌.

  • 빈 : 자기가 컨테이너 내부에 만든 객체들

    • 빈 설정
      • 이름 또는 ID
      • 타입
      • 스코프

    OwnerController와 OwnerRepository를 예로 들면, 둘은 다 ApplicationContext 내부에서 만들어 주는 빈! 그래서 둘의 의존성은 Ioc 컨트롤러가 관리해 줌. 오로지 빈만 관리 가능.

    하지만, Owner는 빈이 아님. 어떻게 알 수 있냐? => 인텔리제이에서 오른쪽에 초록 콩이 붙어있으면 빈ㅎㅎ

    Ioc 컨테이너 자체도 빈으로 되어있음. 자기자신이. 따라서 볼려면 가져와서 볼 수 있음. 하지만 보통 직접 쓸일도, 볼일도 딱히 없음.

  • IOC는 DI와 DL에 의해 구현됨.

3. Bean

: 스프링 IoC 컨테이너가 관리하는 객체.

  • 어노테이션 : 자바의 어노테이션은 소스코드에 추가해서 사용할 수 있는 메타 데이터의 일종. 주석처럼 기능이 없어 아무 동작을 하지 않음.
    • 어노테이션을 마커로 사용해서 어노테이션을 처리하는 것이 있는 것.
  • 등록하는 방법이 크게 두가지.
  1. Compoennt Scanning
  • @Component
    • @Repository
    • @Service
    • @Controller
    : Springboot에서는 SpringbootApplication이라는 어노테이션을 가진 것이 분명히 존재함. 이 어노테이션을 따라가보면 ComponentScan을 찾을 수 있음. 역할은 Component Scan 어노테이션을 처리하는 핸들러의 역할. 물론 구체적 설정에 따라 달라지지만, 대부분 모든 패키지에 Component라는 어노케이션이 붙어있는 class를 찾아 bean으로 등록해 줌.-> 그럼 controller가 bean이 되는 이유? controller를 클릭해서 좀 더 열어보면 결국 @component가 붙어있음. 결국 controller는 component와 같은 것.
  1. 직접 하나하나 등록
    @Bean
    public String keesun(){ // 메서드 이름이 빈의 이름이 됨.
    	return "keesun";
    }
    단, 이렇게 직접 bean을 등록하기 위해선 Configuration이라는 어노케이션을 가진 class안에 적용해야 함.
  • 빈을 꺼내서 쓰는 방법은?

    @RestController
    	public class SampleController {
    
    	//@Autowired를 사용하여 bean을 꺼내 쓸 수 있음.
    
    	@Autowired
    	String keesun;
    
    	@GetMapping("/context")
    	public String context(){
    		return "hello" + keesun;
    	}
    }

    point. 오로지 빈들만 의존성을 관리해 줌!!!
    빈을 사용하고 싶으면, 해당 class도 bean이 되어야 함.
    오로지 bean만 bean을 의존할 수 있음

  • Bean Scope
    : Bean이 존재할 수 있는 범위

    • 스프링 컨테이너에서 함께 시작되어 종료될 때까지 스프링이 관리해주는데 이유는 스프링 빈들을 singleton scope로 관리되기 때문.

    • singleton

      • 스프링 IoC컨테이너 안에 단 한 번 생성되어 캐시에 저장됨.
      • 모든 후속 request와 bean 이름에 대한 참조는 캐시된 객체를 반환.
      • 기본적으로 모든 bean은 scope이 명시되어 있지 않으면 singleton임.
    • prototype

      • 모든 요청에서 새로운 객체를 생성하여 주입.
      • stateful한 bean에는 prototype scope를 사용해야하며, stateless한 bean에는 singleton scope를 사용함.
      • 만일 프로토타입 빈에 종속된 싱글톤 빈을 사용하면, 인스턴스화 시 종속성이 해결 됨. 싱글톤 빈에 프로토타입 빈을 주입할 때, 먼저 프로토타입 빈이 인스턴스화 되고나서싱글톤 빈에 주입됨. 그 프로토타입 빈은 싱글톤 빈에게 제공되는 유일한 인스턴스가 됨.

4. 의존성 주입(Dependency Injection)

: 필요한 의존성을 어떻게 받아올 것인지?

  • 객체가 서로 의존하는 관계가 되게 의존성을 주입함.

  • 객체지향에서의 의존성 : 하나의 객체가 어떠한 다른 객체를 사용하고 있음을 의미.

  • IOC에서의 DI : 각 클래스 사이에 필요로 하는 의존관계를 빈 설정 정보를 바탕으로 컨테이너가 자동으로 연결해 줌.

  • @Autowired/ @Inject를 어디에 붙일지?

    • 생성자 : 어떠한 class에 반드시 필요한 의존성이라면 생성자를 추천
    • 필드 : setter가 없다면 필드에 붙이기
    • Setter : setter를 가지고 있다면, setter에 붙이기 (없는데 굳이 만들어서 붙일 필요는 없음.)
      보통 여기 3개에 붙임.
    • 어떤 bean 클래스에 생성자가 오로지 하나만 있고, 그 생성자의 매게변수 타입이 빈이라면(매개변수는 여러개 와도 상관없음. 매개변수가 다 빈이면 됨.), @Autowired가 없더라도 그 빈을 주입해줌. (생략가능) => 오히려 깔끔함.
      -> 하지만 이 개념을 모르는 분들에겐 혼란스러울 수 있음.

5. 의존성 검색(Dependency Lookup)

: 컨테이너에서는 객체들을 관리하기 위해 별도의 저장소에 빈을 저장하는데, 개발자들이 컨테이너에서 제공하는 API를 이용해 저장소에 저장되어있는 사용하고자 하는 빈을 검색하는 방법.

다른 특징

1. AOP(Abstract Oriented Programming)

: 흩어진 코드를 한 곳으로 모으는 코딩 기법.

  • 관점 지향 프로그래밍.

  • 대부분의 소프트웨어 개발 프롯스는 OOP를 사용.

  • OOP는 객체지향 원칙에 따라 관심사가 같은 데이터를 한곳에 모아 분리하고 낮은 결합도를 갖게 하여 독립적이고 유연한 모듈로 캡슐화를 하는 것.
    -> 하지만, 중복된 코드들이 많아져 가독성, 확장성, 유지보수성을 떨어뜨림. (?)
    -> 자바의 장점과 정반대되는거 아닌가

  • 이러한 문제를 보완하기 위해 핵심기능과 공통기능을 분리시켜 핵심 로직에 영향을 끼치지 않게 공통기능을 끼워 넣는 개발 형태로, 무분별하게 중복되는 코드를 한 곳에 모아 중복 되는 코드를제거할 수 있음.

  • 공통 기능을 한 곳에 보관하여 공통 기능 하나의 수정으로 모든 핵심기능들의 공통기능을 수정할 수 있어 효율적인 유지보수가 가능하며 재활용성이 극대화 됨.

  • 물론 OOP로도 AOP를 구현 가능하나, spring에서는 좀 더 편리하게 AOP를 사용할 수 있도록 지원중임.

  1. 바이트 코드 조작
  2. 프록시 패턴 사용 (spring AOP는 보통 프록시 패턴을 사용)

2. PSA(Portable Service Abstraction)

: 잘 만든 인터페이스, 일관된 서비스 추상화

MySQL을 사용하다 Maria DB로 데이터베이스를 바꿔야하는 상황에서 데이터베이스마다 사용방법이 다르면? 아마 기존에 작성한 코드를 전부 지우고 새로 작성하거나, 다른 코드를 모두 찾아 일일이 수정해 주어야 함.

-> 그러나 스프링을 사용하면, 동일한 사용법을 유지한 채로 데이터베이스를 바꿀 수 있음.
- 스프링이 데이터베이스 서비스를 추상화한 인터페이스를 제공해주기 대문에 가능!

  • 즉, 스프링은 Java를 사용하여 데이터베이스에 접근하는 방법을 규정한 인터페이스인 JDBC(Java DataBase Connectivity)를 제공하고 있음! 이러한 JDBC를 사용하면 이후 데이터베이스를 바꿔도 기존 작성한 데이터베이스 접근 로직을 그대로 사용할 수 있음.

    • 각 디비 회사들은 자신의 디비에 접근하는 드라이버를 Java 코드 형태로 배포
      -> 이 드라이버에 해당하는 Java 코드의 클래스가 JDBC를 구현함.

    이처럼 특정 기술과 관련된 서비스를 추상화하여 일관된 방식으로 사용될 수 있도록 한 것을 PSA라고 함.

3. 스프링 캐시

@EnableCaching으로 사용.

@Cacheable, @CacheEvict 등등을 사용 가능.
CacheManager를 사용.
JCacheManager, ConcurrentMapCacheManager, EhCacheCacheManager ...

4. 스프링 웹 MVC

MVC란 Model view controller 구조로, 사용자 인터페이스와 비지니스 로직을 분리하여 개발하는 것.

  • MVS에서는 Model1과 Model2로 나누어져 있고, 일반적인 MVC는 Model2를 지칭함.
  • Model
    : 데이터 처리를 담당하는 부분. View와 Controller의 어떠한 정보도 가지고 있어서는 안됨.
    • Service영역 : 불필요한 HTTP 통신을 하지 않아야하며, request나 response와 같은 객체를 매개변수로 받으면 안됨. 또한, view에 종속적인 코드가 없어야하며, View가 변경되더라도 Service부분은 그대로 재사용 가능해야 함.
    • DAO 영역
  • View
    : 사용자 interface를 담당하며, 사용자에게 보여지는 부분. View는 Controller를 통해 모델 데이터에 대한 시각화를 담당하며, View는 자신이 요청을 보낸 Controller의 정보만 알고 있어야 하는 것이 핵심.
    • Model이 가지고 있는 정보를 저장해서는 안되며, Model과 Controller에 구성요소를 알아서는 안됨.
  • Controller
    : View에 받은 요청을 가공하여 Model(Serviec영역)에 이를 전달. 또한, Model로부터 받은 결과를 View로 넘겨주는 역할.
    • 모든 요청 에러와 모델 에러를 처리하며 View와 Controller에 정보를 알고 있어야 함.
    • Model과 View의 정보에 대해 알고 있어야 함.
  • Model, View, Controller를 나누는 이유
    : 소스를 분리함으로서 각 소스의 목적이 명확해지고, 유지보수 하는데 있어서 용이해짐.
    • Model의 Service영역은 자신을 어떠한 controller가 호출하든 상관없이 정해진 매개변수만 받으면 자신의 비지니스 로직을 처리할 수 있어야 함. (모듈화를 통해 어디서든 재사용이 가능해야 함.)
    • 즉, View의 정보가 달라지더라도 Controller에서 Service에 넘겨줄 매개변수 데이터 가공만 처리하면 되기때문에 유지보수 비용을 절감할 수 있는 효과가 있음.
    • Service영역의 재사용이 용이하기 때문에 확장성 부분에서도 큰 효과를 볼 수 있다는 장점이 있음.

우리의 코드는 서블릿일수도 있고 리엑티브일수도 있음. 이건 코드를 봐선 모르고 의존성을 확인해봐야함.

요약

스프링의 핵심은 IOC, AOP, PSA. 대부분의 라이브러리는 PSA. spring framework가 제공해주고 있는 api는 90프로가 PSA로 추상화되어있는 abstraction 계층. 조금 더 유연한 코드를 작성할 수 있도록 좋은 인터페이스를 제공.

IoC는 OwnerControlloer가 OwnerRepository를 사용할 때, 오너컨트롤러가 직접 만들어서 사용하는 것이 아닌 누군가로부터 주입을 받아 디펜던시가 (오너레파지토리가 생성자를 통해) 들어온다 가정하고 코딩을 하면 됨. 실제 오너레파지토리 의존성을 주입해주는건 스프링이 하는 일! 구체적으로는 spring의 ApplicationContext가 해주는 것.
-> 왜 이렇게 할까?
: test를 작성하기가 더 쉬움.

AOP는 사방으로 흩어져서 존재하는 코드들이 있음. 다양한 인증, 권한확인 같은 것들을 한곳으로 모아 코딩할 수 있도록 해주는 프로그램 기법. 그 중 spring APO는 프록시를 사용해서 구현.

PSA는 대부분의 스프링이 제공해주는 api들이 PSA임. 컨트롤러, getMapping()같은 것들도 전부.


spring boot

1. spring boot란?

: 스프링 부트는 단독적이고, 상용화 수준의 스프링 기반 어플리케이션을 단지 실행할 수 있을 정도로 쉽게 만들 수 있음.

  • 기존 스프링 프레임워크 위에 구축됨.
  • 스프링 부트를 사용하면 독립실행형과 프로덕션 등급 스프링 기반 어플리케이션을 쉽게 만들 수 있음.
  • 설정의 많은 부분을 자동화하여 사용자가 편하게 스프링을 활용할 수 있도록 도와줌.

2. spring과 spring boot의 차이

  • spring framework의 경우, dependency를 설정해 줄 때 설정 파일이 매우 길고, 모든 dependency에 대해 버전 관리도 하나하나 해주어야 함.

    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-web</artifactId>
       <version>5.3.5</version>
    </dependency>
    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-webmvc</artifactId>
       <version>5.3.5</version>
    </dependency>

    하지만, spring boot의 경우에는 dependency를 spring framework보다 쉽게 설정해 줄 수있으며, 버전 관리도 자동으로 해줌.

    implementation 'org.springframework.boot:spring-boot-starter-web'

    더해서, test 프레임워크를 사용하고자 하는 경우 spring framework의 경우에는 spring test, JUnit, Hamcrest, Mockito 등 모든 라이브러리를 추가해줘야 하지만, spring boot에서는 spring boot starter test만 추가해주면 됨.

  • configuration
    : spring framework의 경우 configuration을 설정할 때도 매우 길고, 모든 어노테이션 및 빈 등록을 설정해 주어야 함.
    - Thymeleaft 템플릿을 사용하기 위한 코드 작성 예시)

    @Configuration
    @EnableWebMvc
    public class MvcWebConfig implements WebMvcConfigurer {
    
     @Autowired
     private ApplicationContext applicationContext;
    
     @Bean
     public SpringResourceTemplateResolver templateResolver() {
         SpringResourceTemplateResolver templateResolver = 
           new SpringResourceTemplateResolver();
         templateResolver.setApplicationContext(applicationContext);
         templateResolver.setPrefix("/WEB-INF/views/");
         templateResolver.setSuffix(".html");
         return templateResolver;
     }
    
     @Bean
     public SpringTemplateEngine templateEngine() {
         SpringTemplateEngine templateEngine = new SpringTemplateEngine();
         templateEngine.setTemplateResolver(templateResolver());
         templateEngine.setEnableSpringELCompiler(true);
         return templateEngine;
     }
    
     @Override
     public void configureViewResolvers(ViewResolverRegistry registry) {
         ThymeleafViewResolver resolver = new ThymeleafViewResolver();
         resolver.setTemplateEngine(templateEngine());
         registry.viewResolver(resolver);
     }
    }

    대신 spring boot에서는 applicationproperties파일이나 application.yml파일에 설정하면 됨.

    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf
  • AutoConfiguration
    : spring frame과 달리 spring boot에는 autoconfiguration이라는 것이 있음.
    spring boot로 실행할 수 있는 어플리케이션을 만들기 시작하면, 클래스에 @SpringBootAplication이라는 어노테이션을 확인할 수 있음. 이를 통해 많은 외부 라이브러리, 내장 톰캣 서버 등이 실행될 수 있음.

    • 만일 제거하고 실행할 경우, 일반적인 자바 프로그램과 동일하게 실행됨.
    • 어노테이션 : 주석, 추후 특정 어노테이션을 처리하는 컴파일러가 어노테이션을 읽으면 알맞은 처리를 진행함. 기능이 포함된 것이 아님. 프로그램 곳곳에 분산된 기능을 한 곳에 모아 처리하고 싶을 때 사용하기도 함.

  • @ComponentScan
    : @Component, @Controller, @Repository, @Service라는 어노테이션이 붙어있는 객체들을 스캔하여 자동으로 Bean에 등록해줌.

  • @EnableAutoConfiguration
    : @ComponentScan 이후 사전에 정의한 라이브러리들을 Bean에 등록해줌.

  • 편리한 배포
    : spring framework로 개발한 어플리케이션의 경우, war 파일을 web application server에 담아 배포한반면, spring boot framework는 Tomcat이나 Jetty 같은 내장 WAS를 가지고 있어, jar 파일로 간편하게 배포 가능.

    이러한 장점들을 통해 spring boot framework는 spring framework보다 개발자가 더더욱 개발에만 집중할 수 있도록 도와줌.

    요약하자면 spring boot는 표준 spring 프레임워크의 모든 기능을 포함하는 동시에 어플리케이션 개발을 훨씬 쉽게 만들어주는 프레임워크임.
    spring과 비교할 때 모든 spring boot의 속성이 자동 구성되기 때문에 훨씬 더 짧은 시간에 어플리케이션을 시작하고 실행할 수 있음.

참고자료

profile
좋은 사람들과 좋은 시간을 보내기 위한 프론트엔드 개발자

0개의 댓글