728x90
반응형

설정을 추가해준다.

main/resources/apllication.properties

...

server.port=8083

...
728x90
반응형
728x90
반응형

1. Lombok

  필드에 @Autowired 주입 처럼 편하게 사용하는 방법을 제공하는 lombok 라이브러리가 있다.

 

2. build.gradle 파일을 수정한다.

/build.gradle

plugins {
	id 'org.springframework.boot' version '2.7.3'
	id 'io.spring.dependency-management' version '1.0.13.RELEASE'
	id 'java'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

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

	//lombok 라이브러리 추가 시작
	compileOnly 'org.projectlombok:lombok' 
	annotationProcessor 'org.projectlombok:lombok'
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
	//lombok 라이브러리 추가 끝


	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

3. Plugins 를 확인한다.

Preferences -> plugins 검색 -> lombok 검색 -> installed 에 있는지 확인 ( 없으면 설치 )

 

4. Annotation processors 를 설정한다.

 

5. @Getter @Setter Annotation을 확인해본다.

package hello.core;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class HelloLombok {

    private String name;
    private int age;

    public static void main(String[] args) {
        HelloLombok helloLombok = new HelloLombok();
        helloLombok.setName("teepo");

        System.out.println(helloLombok.getName());
    }
}

 

이렇게 따로 Getter, Setter 를 선언해주지 않아도 사용할 수 있다.

 

String에서도 유용한 상황이 있는데,

 

예를 들어 아래의 코드가 있을 때

@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    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);
    }

    // test
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

 

변수 선언에 final 을 붙이고 생성자를 Annotation으로 만들어 줄 수 있다.

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final 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);
    }

    // test
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

 

생성자가 한 개였기 때문에 @Autowired 옵션이 자동으로 붙었다.

728x90
반응형
728x90
반응형

1. 싱글톤 패턴이란?

하나의 인스턴스만 만들고, 그 인스턴스를 사용하는 디자인 패턴

2. 스프링 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
  • 스프링 컨테이너는 지저분한 코드가 들어가지 않아도 된다.

3. 싱글톤 패턴의 주의점

  • 여러 클라이언트가 하나의 같은 객체를 공유하기때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.
  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
  • 가급적 읽기만 가능해야 한다.
  • 변수를 따로 할당해주어서 쓰거나 해야지 직접 값을 변경하는 방법은 좋지 않다.

 

4. @Configration , 싱글톤

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemoryMemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(),discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }

}

이런 식의 코드가 있을 때 일반적인 생각으로는 AppConfig 파일이 불러와질 때 memberRepository는 3번이 불러와져야 한다.

하지만 스프링이 싱글톤 패턴을 적용시켜서 한 번만 불러와진다.

 

5. 바이트코드 조작법

AppConfig 를 로그로 찍어보면 CGLIB이 붙은 것을 볼 수 있다.

자바 코드를 바이트 코드로 바꿔서 스프링 컨테이너에 등록을 하는 것을 알 수 있다.

바이트 코드를 비교해서 이미 등록이 된 bean 을 두 번 반환하지 않고 한 번만 반환하는 것을 알 수 있다.

 

이는 @Configuration Annotation 을 사용해야지만 바이트 코드를 사용한 싱글톤이 보장됨을 의미한다.

 

 

 

728x90
반응형
728x90
반응형

1. IoC ( 제어의 역전 )

  •  AppConfig 파일을 만들면 프로그램의 흐름을 직접 구현되는 곳이 아닌 외부에서 설정해줄 수 있다.
  • 이렇듯 외부에서 프로그램 제어 흐름을 제어하는 것을 제어의 역전이라 한다.

 

2. DI ( 의존관계 주입 )

  • 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스)의 의존관계 둘을 분리해서 생각해야 한다.
    • 정적인 클래스는 인터페이스를 의존한다.
    • 애플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라 한다. ( AppConfig 파일에서 사용 ) 
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

 

3. 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.

 

4. 스프링 컨테이너

  • ApplicationContext를 스프링 컨테이너라 한다.
  • 스프링 컨테이너는 @Configration이 붙은 AppConfig 를 설정 정보로 사용한다.
  • 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean이 붙은 메서드의 명을 이름으로 사용한다.
  • applicationContext.getBean() 메서드로 빈을 찾을 수 있다.

 

728x90
반응형
728x90
반응형

1. SRP ( 단일 책임 원칙 )

 - 클래스와 메서드는 하나의 역할만 하도록 한다.

 - 각자의 책임만 다하면 된다.

 

2. OCP ( 개방 폐쇠 원칙 )

 - 자신의 확장에는 개방되어있고, 주변의 변화에 대해서는 폐쇄돼있어야 한다.

 - 별도의 설정자를 지정해주어야 지킬 수 있다.

 

3. LSP ( 리스코프 치환 원칙 )

- 서브타입은 언제나 자신의 상위 타입으로 교체할 수 있어야 한다.

 

4. ISP ( 인터페이스 분리 원칙 )

 - 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

 

5. DIP ( 의존 역전 원칙 )

 - 자신보다 변하기 쉬운 것에 의존하지 말아야 한다.  

 - 인터페이스에 의존해야지, 구체화에 의존하면 안된다.

728x90
반응형
728x90
반응형

1. H2 Database 

https://www.h2database.com 

 

H2 Database Engine (redirect)

H2 Database Engine Welcome to H2, the free SQL database. The main feature of H2 are: It is free to use for everybody, source code is included Written in Java, but also available as native executable JDBC and (partial) ODBC API Embedded and client/server mo

www.h2database.com

 

해당 경로로 이동하여 권한 설정 ( /bin )

$ chmod 755 build.sh

 

실행

$ ./h2.sh

 

home 경로에 test.mv.db 파일 생성 확인

 

2. JPA

  • JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
  • JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.
  • JPA를 사용하면 개발 생산성을 크게 높일 수 있다.

1. build.gradle 파일에 JPA, h2 데이터베이스 라이브러리를 추가해준다.

/build.gradle

 dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	runtimeOnly 'com.h2database:h2'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

 

2. 스프링 부트에 JPA 설정을 추가해준다.

resources/application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
  • show-sql : JPA가 생성하는 SQL을 출력한다.
  • ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끈다.  create 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다. 해보자.

 

3. JPA 엔티티 매핑

/com.example.demo/domain/Member

package com.example.demo.domain;

import javax.persistence.*;

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    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;
    }
}

 

4. JpaMemberRepository 생성

/repository/JpaMemberRepository

package com.example.demo.repository;

import com.example.demo.domain.Member;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

public class JpaMemberRepository implements MemberRepository {

    private final EntityManager em;

    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public Member save(Member member) {
        em.persist(member);
        return null;
    }

    @Override
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
        return result.stream().findAny();
    } 

    @Override
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
}

 

5. 서비스 계층에 트랜잭션 추가

import org.springframework.transaction.annotation.Transactional;

@Transactional
public class MemberService {
  •  스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다. 만약 런타임 예외가 발생하면 롤백한다.
  • JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.

 

3. 스프링 데이터 JPA

 

1. 스프링 데이터 JPA 회원 레포지토리

/repository/SpringDataJpaMemberRepository

package com.example.demo.repository;

import com.example.demo.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface SpringDataJpaMemberRepository extends JpaRepository<Member,Long>, MemberRepository {
    Optional<Member> findByName(String name);
}

 

2. 스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경

package com.example.demo;

import com.example.demo.repository.MemberRepository;
import com.example.demo.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }
}

 

 

4. AOP

  • 모든 메소드의 호출 시간을 측정하고 싶다면?
  • 공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern)
  • 회원 가입 시간, 회원 조회 시간을 측정하고 싶다면?

1. AOP를 등록한다.

com.example.demo/aop/TimeTraceAop

package com.example.demo.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class TimeTraceAop {
    @Around("execution(* com.example.demo..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString());
        try {
            return joinPoint.proceed();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
        }
    }
}

 

728x90
반응형
728x90
반응형

1. Basic Controller

controller 파일을 만들어준다.

/src/main/java/com.example.demo/controller/TeepoController

 

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class TeepoController {

    @GetMapping("teepo-string")
    @ResponseBody
    public String teepoString(@RequestParam("name") String name) {
        return "hello-template" + name;
    }
}

여기서 @ResponseBody 는 API 응답으로 Body 안에 원하는 데이터를 넣어주겠다는 뜻이다.

 

브라우저에서 접속해보면

return 값이 정상적으로 반환되는 것을 확인할 수 있다.

 

 

2. JSON return Controller

json 형식으로 반환할 땐 아래와 같이 할 수 있다.

    @GetMapping("teepo-api")
    @ResponseBody
    public Teepo teepoApi(@RequestParam("name") String name) {
        Teepo teepo = new Teepo();
        teepo.setName(name);
        return teepo;
    }
    static class Teepo {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }

 

서버를 키고 확인해보면,

이와같이 json 형식으로 데이터를 받은 것을 확인할 수 있다.

 

3. Member 객체 생성

domain 폴더를 만들고 Member 에 대한 객체를 만들어보자.

com.example.demo/domain/Member

package com.example.demo.domain;

public class Member {
    private Long id;
    private String name;

    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;
    }
}

 

멤버 리포지토리 인터페이스를 만든다.

com.example.demo/repository/MemberRepository

package com.example.demo.repository;

import com.example.demo.domain.Member;
import java.util.List;
import java.util.Optional;


public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

여기서 Optional 이란 null 값을 처리하기 위해 한 번 감싸서 선언함을 의미한다.

 

리포지토리 메모리 구현체를 파일을 만든다.

com.example.demo/repository/MemoryMemberRepository

package com.example.demo.repository;

import com.example.demo.domain.Member;

import java.util.*;

/**
 * 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
 */
public class MemoryMemberRepository implements MemberRepository {
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;
    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }
    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }
    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }
    public void clearStore() {
        store.clear();
    } 
}

 

4. Member Test

/src/text/java/com.example.demo/repository/MemoryMemberRepositoryTest

package com.example.demo.repository;

import com.example.demo.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;

import static org.assertj.core.api.Assertions.*;

class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();
    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }
    @Test
    public void save() {
        //given
        Member member = new Member();
        member.setName("spring");
        //when
        repository.save(member);
        //then
        Member result = repository.findById(member.getId()).get();
        assertThat(result).isEqualTo(member);
    }
    @Test
    public void findByName() {
        //given
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);
        //when
        Member result = repository.findByName("spring1").get();
        //then
        assertThat(result).isEqualTo(member1);
    }
    @Test
    public void findAll() {
        //given
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);
        //when
        List<Member> result = repository.findAll();
        //then
        assertThat(result.size()).isEqualTo(2);
    }
}

여기서 get 메소드는 Optional 객체 에서 데이터를 빼오는 방법이다.

 

5. Member Service

/src/main/java/com.example.demo/service/MemberService

package com.example.demo.service;

import com.example.demo.domain.Member;
import com.example.demo.repository.MemberRepository;
import com.example.demo.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new
            MemoryMemberRepository();
    /**
     * 회원가입
     */
    public Long join(Member member) {
        validateDuplicateMember(member); //중복 회원 검증 memberRepository.save(member);
        memberRepository.save(member);
        return member.getId();
    }
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });

    }
    /**
     *전체 회원 조회
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    } }

 

6. Member Service Test

먼저 회원 서비스 코드를 DI 가능하게 변경한다.

/src/main/java/com.example.demo/service/MemberService

    ...
    
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
    ...

 

클래스 안에서 마우스를 클릭하고 command + shift + t 를 누르면 Test를 자동으로 생성해줄 수 있다.

 

/src/test/java/com/example.demo/service/MemberServiceTest

package com.example.demo.service;

import com.example.demo.domain.Member;
import com.example.demo.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
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;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }
    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }
    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }
    @Test
    public void 중복_회원_예외() throws Exception {
        //Given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");

        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));//예외가 발생해야 한다.

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

 

 

7. 컴포넌트 스캔과 자동 의존관계 설정

Service 를 사용하는 Controller를 만들어 보자.

/src/main/java/com.example.demo/controller/MemberController

package com.example.demo.controller;

import com.example.demo.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

생성자에 @Autowired 가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다. 이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI (Dependency Injection), 의존성 주입이라 한다.

@Controller 가 있으면 스프링 빈으로 자동 등록된다.

 

 

Service 와 Repository에도 annotation 을 달아주자.

...
@Service
public class MemberService {
...

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
...

 

...
@Repository
public class MemoryMemberRepository implements MemberRepository {
...

 

8. 자바 코드로 직접 스프링 빈 등록하기

회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 애노테이션을 제거하고 진행한다.

 

/src/test/java/com.example.demo/SpringConfig

package com.example.demo;

import com.example.demo.repository.MemberRepository;
import com.example.demo.repository.MemoryMemberRepository;
import com.example.demo.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

이렇게 하면 스프링이 실행되면서 의존성을 주입해준다.

728x90
반응형
728x90
반응형

1. Java 11 과 intelliJ를 설치한다.

 

https://www.oracle.com/java/technologies/downloads/#java11-mac

 

Download the Latest Java LTS Free

Subscribe to Java SE and get the most comprehensive Java support available, with 24/7 global access to the experts.

www.oracle.com

 

https://www.jetbrains.com/idea/download/#section=mac

 

Download IntelliJ IDEA: The Capable & Ergonomic Java IDE by JetBrains

Download the latest version of IntelliJ IDEA for Windows, macOS or Linux.

www.jetbrains.com

 

2. 스프링 부트 스타터 사이트로 이동하고 프로젝트를 만든다.

https://start.spring.io/

 

 

3. 아래처럼 생성해주고, intelliJ 에서 build.gradle 파일로 프로젝트를 열어준다.

 

4. build 버튼을 눌러서main 함수를 실행해본다.

터미널 로그를 확인해보면 8080 포트에 서버가 동작하는 것을 확인할 수 있다.

 

5. localhost:8080 으로 들어가본다.

 

이렇게 뜨면 잘 된 것이다.

 

참고로 아래와 같이 설정을 해두면 기존보다 빨리 build 할 수 있다.

 

6. 화면을 보여주기위해 html파일을 만들어보자.

src/main/resources/static/index.html

<!DOCTYPE HTML>
    <html>
<head>
    <title>Hello</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /
</head>
<body>
Hello
<a href="/hello">hello</a>
</body>
    </html>

 

다시 서버를 실행해보면,

잘 뜨는 것을 확인할 수 있다.

 

728x90
반응형
728x90
반응형
.env
node_modules
pakage-lock.json
config.json

# Logs
logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test
.env.production

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# vscode
.vscode

# Dev Files
ConstantLocal.js
RESTLocal.js
728x90
반응형
728x90
반응형

1. jest 모듈을 설치한다.

$ npm i jest @types/jest ts-jest

 

2. jest 설정파일을 만든다.

/jest.config.ts

module.exports = {
  "testMatch": [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)"
  ],
  "transform": {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
}

 

3. pakage.json 에 테스트 script를 수정해준다.

  "scripts": {
    "test": "jest",
    "start": "node dist/app.js",
    "build": "tsc -p .",
    "dev": "nodemon --watch \"src/**/*.ts\" --exec \"ts-node\" src/app.ts"
  },

 

4. test 파일을 만들고 아래처럼 테스트 파일을 만들어준다.

 

/tests/routers/one.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import { logger } from '../../config/winston';

// node-json-db
import { db } from '../../config/nodejsondb';

import { 
    getDriverHandler,

    alertCloseAccept,
    alertCloseDismiss,
    promptCloseHandler,

    addCookie,
    getOneCookie,
    getAllCookie,
    deleteOneCookie,
    deleteAllCookie,

    fileRegister,
    
    findElementById,
    findElementByName,
    findElementByXpath,
    findElementByCss,

    JqChangeValueByID,
    JqRemoveAttribute,

    naviGet,
    naviBack,
    naviForward,
    naviRefresh,

    popupClose

 } from '../../modules';

 describe('GET /one', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeAll(async () => {
        driver = await getDriverHandler();
    })

    afterAll(async () => {
        await driver.quit()
    })

    test('url 잘 뜨는지 확인', async () => {
        try {
            // 브라우저에 접속
            await driver.get('https://typo.tistory.com/');

            // 현재 주소 가져오기
            const text = await driver.getCurrentUrl();

            // test
            expect(text).toEqual('https://typo.tistory.com/')
        }
        catch(Err) {
            console.log(Err)
            logger.debug(Err)
            throw Error;
        }
    })

 })

 

 

/tests/middlewares/http.test.ts

import express, { Request, Response, NextFunction } from 'express';
import {   
    loggerInfo,
    loggerError,
    loggerHttp,
    loggerDebug,
 } from '../../config/winston'
import { httpLoggingMiddleware } from '../../middlewares';

describe('MiddleWare http', () => {
    let mockRequest : Partial<Request>;
    let mockResponse : Partial<Response>;
    let nextFunction: NextFunction = jest.fn();

    beforeEach(()=> {
        mockRequest = {};
        mockResponse = {
            json: jest.fn()
        };
    });

    test('http 로깅 미들웨어 테스트', async () => {
        try {
            await httpLoggingMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);
            
            // nextFunction 까지 잘되는지 확인
            expect(nextFunction).toBeCalledTimes(1);
        }
        catch (Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

reqest, response, next 를 mock으로 만들어 테스트해준다.

 

/tests/middlewares/timeInterval.ts

import express, { Request, Response, NextFunction } from 'express';
import {   
    loggerInfo,
    loggerError,
    loggerHttp,
    loggerDebug,
 } from '../../config/winston'

import { timeIntervalMiddleware } from '../../middlewares';

describe('MiddleWare timeInterval', () => {
    let mockRequest : Partial<Request>;
    let mockResponse : Partial<Response>;
    let nextFunction: NextFunction = jest.fn();

    beforeEach(()=> {
        mockRequest = { originalUrl : '/one' };
        mockResponse = {
            json: jest.fn()
        };
    });

    test('timeInterval 로깅 미들웨어 테스트', async () => {

        try {
            await timeIntervalMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);

            // nextFunction 까지 잘되는지 확인
            expect(nextFunction).toBeCalledTimes(1);
        }
        catch (Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

/tests/modules/db/expireHandler.ts

import express, { Request, Response, NextFunction } from 'express';

// node-json-db
import { db } from '../../../config/nodejsondb';

// logger
import { loggerDebug } from '../../../config/winston';
import { expireHandler } from '../../../modules';

describe('Module expireHandler', () => {
    let dbPath : string;

    beforeEach(()=> {
        dbPath = '/one'
    });

    test('30분 지난 데이터 삭제 및 확인', async () => {
        try {
            let nowTime = new Date(); // 현재 시간

            // 30분 지난 데이터 삭제
            await expireHandler(dbPath);

            // 데이터 베이스 안 데이터들
            const datalist = await db.getData(dbPath);

            let index = 0; // 기준점이 되는 index

            // 데이터 매핑 및 시간 차이 계산해서 30분 지난 데이터는 삭제
            await datalist.map(async (item : any, index2: number) => {
                let beforeTime =  new Date(item.date); // 데이터들 저장된 시간
                let diff = (nowTime.getTime() - beforeTime.getTime()) / (1000*60); // 데이터들 시간 차이
                // 저장된 지 30분이 지나면 index 체크
                if(diff > 30) {
                    index = index2;
                }
            })

            // ----- 30분 지난 데이터 없는지 확인 -----
            expect(index).toEqual(0);

        }
        catch(Err) {
            console.log("Err : ",Err)
            loggerDebug.info(JSON.stringify(Err))
        }
    })
})

 

/tests/modules/selenium/alertHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
} from '../../../config/winston';

import { 
    getDriverHandler,
    findElementById,

} from '../../../modules';

 // window type 선언
declare const window: typeof globalThis;

 describe('Module alertHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/alerts/alert-test.html')
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('alert 테스트 ( alert 텍스트가 잘 나오는지 )', async () => {
        try {
            await (await findElementById(driver,'alertexamples')).click();

            // alert 창 뜰 때까지 기다림
            await driver.wait(until.alertIsPresent());

            // alert 로 드라이버 이동
            let alert = await driver.switchTo().alert();

            // alertText
            let alertText = await alert.getText();

            // 확인버튼 클릭
            await alert.accept();

            // 다시 원래 컨텐츠로 드라이버 이동
            await driver.switchTo().defaultContent();

            expect(alertText).toEqual('I am an alert box!');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('prompt 테스트', async () => {
        try {
            await (await findElementById(driver,'promptexample')).click();

            // alert 창 뜰 때까지 기다림
            await driver.wait(until.alertIsPresent());

            // alert 로 드라이버 이동
            let alert = await driver.switchTo().alert();

            // alert text 사용하는 곳
            let alertText = await alert.getText();

            // 확인버튼 클릭
            await alert.accept();

            // 다시 원래 컨텐츠로 드라이버 이동
            await driver.switchTo().defaultContent();

            expect(alertText).toEqual('I prompt you');

        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })
})

 

/tests/modules/selenium/cookieHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
} from '../../../config/winston';

import { 
    getDriverHandler,

    addCookie,
    getOneCookie,
    getAllCookie,
    deleteOneCookie,

    findElementById,
    findElementByName,

 } from '../../../modules';
  

 describe('Module cookieHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/cookies/adminlogin.html');

        // 로그인 및 쿠키생성
        await (await findElementByName(driver,'username')).sendKeys('Admin');
        await (await findElementByName(driver,'password')).sendKeys('AdminPass');
        await (await findElementById(driver,'login')).click();
    },15000)

    afterEach(async () => {
        await driver.quit()
    })

    test('쿠키 추가하기', async () => {
        try {
            // 쿠키추가
            await addCookie(driver,{name : "name", value : "teepo"});

            // 쿠키 가져오기
            const cookies = (await driver.manage().getCookie('name')).value;

            // 쿠키 확인
            expect(cookies).toEqual('teepo');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 하나 가져오기', async () => {
        try {
            // 쿠키 가져오기
            const result = await getOneCookie(driver,'loggedin');

            // 쿠키 확인
            expect(result.value).toEqual('Admin');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 전부 가져오기', async () => {
        try {
            // 쿠키 가져오기
            const result = await getAllCookie(driver);

            // 쿠키 확인
            expect(result[0].value).toEqual('Admin');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 하나 지우기', async () => {
        try {
            // 쿠키 삭제하기
            await deleteOneCookie(driver,'loggedin');

            // 쿠키 확인
            const result = await getAllCookie(driver);

            // 쿠키가 없어졌는지 확인
            expect(result.length).toEqual(0);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 전부 지우기', async () => {
        try {
            // 쿠키 삭제하기
            await deleteOneCookie(driver,'loggedin');

            // 쿠키 확인
            const result = await getAllCookie(driver);

            // 쿠키가 없어졌는지 확인
            expect(result.length).toEqual(0);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }

    })

})

 

/tests/modules/selenium/driverHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

import { 
    getDriverHandler,
 } from '../../../modules';
  

 describe('Module driverHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('webdriver가 잘 반환되는지 테스트', async () => {
        try {
            driver = await getDriverHandler();
            expect(driver).toBeInstanceOf(WebDriver);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

/tests/modules/selenium/fileHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
} from '../../../config/winston';

import { 
    fileRegister,

    findElementById,
    findElementByName,

    getDriverHandler,

 } from '../../../modules';
  

 describe('Module fileHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/file-upload-test.html')
    })

    afterEach(async () => {
        await driver.quit()
    })

     test('fileRegister', async () => {
         try {
             await fileRegister(driver,By.id('fileinput'),['파일 위치'])
             await (await findElementByName(driver,'fileinput')).click()
             var text = await (await findElementById(driver,'uploadedfilename')).getText()
             expect(text).toEqual('image.jpg')
         }
         catch(Err) {
             loggerDebug.info(JSON.stringify(Err))
         }
     })

})

 

/tests/modules/selenium/findHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

import { 
    getDriverHandler,

    findElementById,
    findElementsById,
    findElementByName,
    findElementByXpath,

    findElementByClass,
    findElementsByName,
    findElementsByXpath,
    findElementsByClass

 } from '../../../modules';
  

 describe('Module findElementHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeAll(async () => {
        driver = await getDriverHandler();
        driver.get('https://testpages.herokuapp.com/styled/find-by-playground-test.html')
    })

    afterAll(async () => {
        await driver.quit()
    })

    test('findElementById', async () => {
        try {
            let text = await (await findElementById(driver,'p1')).getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementByName', async () => {
        try {
            let text = await (await findElementByName(driver,'pName1')).getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementByXpath', async () => {
        try {
            let text = await (await findElementByXpath(driver,'//*[@id="p1"]')).getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementByClass', async () => {
        try {
            let text = await (await findElementByClass(driver,'explanation')).getText();
            expect(text).toEqual('This is a set of nested elements. There are various ways to locate each of the elements. Challenge yourself to find as many ways of locating elements as possible.')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

})


describe('Module findElementsHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeAll(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/find-by-playground-test.html')
    })

    afterAll(async () => {
        await driver.close()
    })

    test('findElementsById', async () => {
        try {
            let text = await (await findElementsById(driver,'p1'))[0].getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementsByName', async () => {
        try {
            let text = await (await findElementsByName(driver,'pName1'))[0].getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementsByXpath', async () => {
        try {
            let text = await (await findElementsByXpath(driver,'//*[@id="p1"]'))[0].getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementsByClass', async () => {
        try {
            let text = await (await findElementsByClass(driver,'explanation'))[0].getText();
            expect(text).toEqual('This is a set of nested elements. There are various ways to locate each of the elements. Challenge yourself to find as many ways of locating elements as possible.');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
        });
})

 

/tests/modules/selenium/jqueryHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

// node-json-db
// import { db } from '../../config/nodejsondb';

import { 
    getDriverHandler,

    findElementById,

    JqChangeValueByID,
    JqSetAttribute,
    JqRemoveAttribute,
 } from '../../../modules';
  

 describe('Module jqueryHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/html5-form-test.html')
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('JqChangeValueByID', async () => {
        try {
            let value = "teepo";
            await (await findElementById(driver,'email-field')).clear()
            await JqChangeValueByID(driver,'email-field',value);
            let text = await (await findElementById(driver,'email-field')).getAttribute('value');
            expect(text).toEqual(value)
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('JqSetAttribute', async () => {
        try {
            await JqSetAttribute(driver,'email-field',['disabled','true']);
            let bool = await (await findElementById(driver,'email-field')).getAttribute('disabled');
            expect(bool).toBe("true")
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('JqRemoveAttribute', async () => {
        try {
            await JqSetAttribute(driver,'email-field',['disabled','true']);
            await JqRemoveAttribute(driver,'email-field','disabled');
            let bool = await (await findElementById(driver,'email-field')).getAttribute('disabled');
            expect(bool).toBeNull()
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

/tests/modules/selenium/navigationHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

// node-json-db
// import { db } from '../../config/nodejsondb';

import { 
    getDriverHandler,

    findElementById,

    naviGet,
    naviBack,
    naviForward,
    naviRefresh,

 } from '../../../modules';
  

 describe('Module navigation', () => {
    // 웹드라이버 설정
    let driver : WebDriver;
    let url = 'https://testpages.herokuapp.com/styled/index.html';

    beforeEach(async () => {
        driver = await getDriverHandler();
        await naviGet(driver,url)
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('naviGet', async () => {
        try {
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual(url);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('naviBack', async () => {
        try {
            await (await findElementById(driver,'basicpagetest')).click();
            await naviBack(driver);
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual(url);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('naviForward', async () => {
        try {
            await (await findElementById(driver,'basicpagetest')).click();
            await naviBack(driver);
            await naviForward(driver);
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual('https://testpages.herokuapp.com/styled/basic-web-page-test.html');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }

    })

    test('naviRefresh', async () => {
        try {
            await naviRefresh(driver);
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual(url);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }

    })

})

 

/tests/modules/selenium/popupHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

import { 
    getDriverHandler,

    findElementById,

    naviGet,

    popupClose

 } from '../../../modules';
  

 describe('Module popupHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await naviGet(driver,'https://testpages.herokuapp.com/styled/alerts/alert-test.html');
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('popupClose', async () => {
        try {
            await (await findElementById(driver,'promptexample')).click();
            popupClose(driver);
            let windowNmber = await (await driver.getAllWindowHandles()).length;
            expect(windowNmber).toEqual(1);
        }
        catch (Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

5. 빌드하고 테스트해본다.

$ npm run build
$ npm run test

728x90
반응형

+ Recent posts