html부터 웹사이트 개발까지! - Spring편

Coosla·2022년 10월 24일
1

✏️ 스프링 개발 환경 구축

Eclipse에 기본적인 스프링 환경 구축하기

  • Eclipse 설치
  • Eclipse 실행 환경 편집
    • Eclipse가 기본적으로 JRE를 이용해 실행, 이후 Lombok 라이브러리 등 사용에 지장이 있을 수 있음.
    • 위 문제를 해결하기 위해 이클립스 폴더 내 'eclipse.ini' 파일 수정
      -vm
      C:\Program Files\Java\...
      // 자신의 컴퓨터에 설치된 JDK 경로를 입력
  • Eclipse에 스프링 플러그인 설치
    • Eclipse 상단 메뉴 Help > Eclipse Marketplace... 선택
    • 검색창에 STS3 키워드 검색
    • 해당 STS3 플러그인 설치
    • 설치 완료되면 Eclipse를 재시작
    • 주의사항
      • 플러그인 설치할 때 MarketPlace에서 게이지바가 다찼다고 플러그인이 설치된 것이 아님.
      • 오른쪽 하단에 전체 설치 진행상황이 표시되므로 이부분을 체크
  • STS(Spring Tool Suite) 세팅
    • STS3 다운로드
    • STS4의 경우 스프링 부트에 특화
    • 스프링 개발은 STS3을 이용, 사용자 계정이 한글 이름으로 지정된 경우 문제가 있으므로 반드시 영어 이름의 계정을 사용
  • Tomcat 서버 설정
    • 톰켓 다운로드
    • Eclipse 상단 메뉴 Window > Preferences 선택
    • Server 세부 항목에 Runtime Environments 선택
    • add 선택해서 Tomcat9 선택, Tomcat이 설치된 경로 지정
  • 프로젝트 생성
    • Spring Legacy Project를 이용해 Maven 기반으로 생성 가능
    • 프로젝트 최소 생성 시 필요한 코드 및 라이브러리를 다운
    • 다운로드한 라이브러리는 사용자 폴더 내 '.m2'라는 이름의 폴더를 이용
    • '.m2' 폴더에 생성된 repository 폴더 안에 다운로드된 라이브러리 파일들이 추가됨.
      • 만약 라이브러리들이 제대로 작동하지 않는다면 repository 폴더를 비우고 프로젝트를 재시작하면 자동으로 프로젝트를 점검하면서 라이브러리를 다시 다운받아줌.
    • Spring Legacy Project 파일 구조
      Spring Legacy Project 파일 구조
      • src/main/java 부분
        • 실제 비즈니스 로직 코드 작업 공간
      • src/main/resources 부분
        • 실행할 때 참고하는 기본 경로
        • 주로 설정 파일을 이곳에서 작업
      • src/test/java 부분
        • 테스팅용 코드를 넣는 작업 공간
      • src/test/resources 부분
        • 테스트 관련 설정 파일 보관 경로
      • JRE System Library
        • Java에서 제공하는 라이브러리
      • Maven Dependencies
        • pom.xml에 기술된 라이브러리
        • pom.xml에 새로운 라이브러리를 기술했을 때 project명에서 마우스 오른클릭 한 후 Maven > Update Project... 선택해야 새로 기술된 라이브러리가 적용, 단축기로 alt + F5
      • Referenced Libraries
        • 프로젝트 Properties > java Build Path, Deployment Assembly에 사용자가 설정한 라이브러리를 가져옴
      • sevlet-context.xml
        • 웹과 관련된 스프링 설정 파일
      • root-context.xml
        • 스프링 설정 파일
      • views 폴더
        • 템플릿 프로젝트의 JSP 파일 경로
      • web.xml
        • Tomcat의 web.xml 파일
      • pom.xml
        • Maven이 사용하는 pom.xml
  • pom.xml을 이용하여 스프링 설정 변경하기
    • 스프링 버전 변경하기

      <properties>
      	...
      	<org.springframework-version>변경할 스프링 버전</org.springframework-version>
      	...
      </properties>
      • 버전을 수정한 뒤 관련 라이브러리를 새롭게 추가하는데 시간이 걸림
      • 관련 라이브러리가 추가된 후 Maven Dependencies에 라이브러리가 제대로 반영됬는지 체크
    • Java Version 변경하기

      • 스프링 5버전 이상부터는 Java를 최소 1.8 이상 사용해야함.
        <plugin>
        	<groupId>org.apache.maven.plugins</groupId>
        	<artifactId>maven-compiler-plugin</artifactId>
        	<version>2.5.1</version>
        	<configuration>
        		<source>변경할 자바 버전</source>
        		<target>변경할 자바 버전</target>
        		<compilerArgument>-Xlint:all</compilerArgument>
        		<showWarnings>true</showWarnings>
        		<showDeprecation>true</showDeprecation>
        		</configuration>
        </plugin>
      • pom.xml을 변경했다면 Maven > update project를 실행

Lombok 라이브러리

  • Lombok이란?

    • Java 라이브러리로 반복되는 setter/getter, toString 등 메소드 작성 코드를 줄여주는 코드 다이어트 라이브러리
    • Lombok의 어노테이션을 이용해 반복되는 메소드를 줄여줌
    • 편의성을 제공하는 라이브러리일 수록 API 설명과 내부 동작을 어느정도 숙지하고 사용하는 것을 권장
      • @data, @ToString 어노테이션으로 자동 생성되는 toString() 메소드는 순환 참조/무한 재귀 호출 문제로 StackOverflowError가 발생할 수도있음.
  • Lombok을 이용한 자바 코드와 일반 자바 코드 차이

    • 일반 자바 코드
      public class 클래스명{
      	private String 변수1;
      	private int  변수2;
      	...
      	
      	public void set변수1(String str){
      		...
      	}
      	public String get변수1(){
      		...
      	}
      
      	public void set변수2(String str){
      		...
      	}
      	public String get변수2(){
      		...
      	}
      	...
      }
    • Lombok을 이용한 자바코드
      @Getter // Getter 메소드를 자동으로 생성
      @Setter // Setter 메소드를 자동으로 생성
      @ToString // ToString 메소드를 자동으로 생성
      @NoArgsConstructor // 파라미터가 없는 생성자를 생성
      @AllArgsConstructor // 클래스에 존재하는 모든 필드에 대한 생성자를 자동으로 생성
      // @RequiredArgsConstructor는 초기화되지 않은 모든 final 필드, @NonNull로 마크되어 있는 필드에 대한 생성자를 자동으로 생성
      // @Data를 사용하면 위 5개의 어노테이션을 포함함.
      public class 클래스명{
      	private String 변수1;
      	private int  변수2;
      	...
      }
  • Lombok 적용 방법

    • 라이브러리 파일을 Class Path 설정하여 라이브러리를 추가하는 방법 존재
    • pom.xml에 의존성 추가하는 방법
      <dependency> 
      	<groupId>org.projectlombok</groupId>            
      	<artifactId>lombok</artifactId>            
      	<version>Lombok 버전</version>            
      	<scope>provided</scope>
      </dependency>
  • Lombok에서 자주 사용하는 어노테이션

    • @Setter/Getter
      • 해당 필드에 대한 기본 getter/setter 메소드를 생성
      • 접근 제한자 설정
        • 따로 설정이 안되어 있으면 public
        • 형태
          @Getter(AccessLevel.PRIVATE)
        • 종류 : PUBLIC, PROTECTED, PACKAGE, PRIVATE
    • @NonNull
      • lombok에서 null-check 로직을 자동으로 생성해주는 애노테이션
    • @NoArgsConstructor
      • 파라미터가 없는 생성자 생성
      • 필드들이 final로 생성되어 있으면 해당 필드를 초기화 할수 없어 생성자를 만들 수 없고 에러 발생
      • @NonNull 같이 필드에 제약 조건이 설정된 경우, 생성자에 null-check 로직을 생성되지 않음
    • @RequiredArgsConstructor
      • 추가 작업을 필요로 하는 필드에 대한 생성자를 생성
      • 초기화 되지 않은 모든 final 필드, @NonNull로 설정된 필드에 대해 생성자를 자동으로 생성
      • @NonNull로 설정된 필드들은 null-check가 추가적으로 생성, 파라미터로 NULL값이 들어오면 생성자에서 NullPointerException이 발생
    • @AllArgsConstructor
      • 클래스에 존재하는 모든 필드에 대한 생성자를 자동으로 생성
      • @NonNull이 설정되어 있으면 null-check로직을 자동적으로 생성

자바 파일로 프로젝트 설정하기

  • 설정 관련 파일 삭제
    • web.xml, servlet-context.xml, root-context.xml 파일 삭제
    • web.xml을 삭제하면 pom.xml 파일에서 오류가 발생
    • pom.xml 수정을 통해 오류 해결
  • pom.xml 수정
    • 추가 코드
      <properties>
      	<java-version>자바버전</java-version>
      	<org.springframework-version>스프링 버전</org.springframework-version>
      ...
      
      <plugin>
      	<groupId>org.apache.maven.plugins</groupId>
      	<artifactId>maven-war-plugin</artifactId>
      	<version>스프링 버전</version>
      	<configuration>
      		<failOnMissingWebXml>false</failOnMissingWebXml>
      	</configuration>
      </plugin>
      ...
  • 설정 자바 파일 생성
    • @Configuration 어노테이션을 이용해 해당 클래스의 인스턴스를 이용해서 설정 파일을 대신함.
    • 형태
      package 패키지 경로
      
      import ...
      
      @Configuration
      public class 클래스명{
      	...
      }

✏️ 스프링 특징과 의존성 주입

스프링 특징

  • POJO(Plain Old Java Object) 기반의 구성

    • 관계를 구성할 때 별도의 API 등을 사용하지 않음.
    • 코드를 개발할 때 개발자가 특정한 라이브러리나 컨테이너의 기술에 종속적이지 않다는 것을 의미
    • 가장 일반적인 형태로 코드를 작성하고 실행할 수 있기 때문에 생산성에서 유리, 테스트 작업도 유연하게 할 수 있음
  • 의존성 주입(DI)을 통한 객체 간의 관계 구성

    • 개발자가 객체와 객체를 분리해서 생성
    • 이 객체들에게 의존성을 부여하는 형태로 개발
    • ApplicationContext가 필요한 객체들을 생성, 의존성 부여하는 역활을 함
    • 각각의 객체간에 의존성이 존재할 경우 프레임워크에서 서로를 연결
    • XML파일로 의존성 주입, 어노테이션을 통해 의존성 주입, Java로 설정하는 방식 등이 있음
  • AOP(Aspect-Oriented-Programming) 지원

    • 시스템이 비즈니스 로직은 아니지만 공통으로 가지고 있는 보안, 로그 등 반드시 처리가 필요한 부분을 스프링에서 '횡단 관심사'라고함
    • 횡단 관심사를 분리해서 모듈로 제작하는 것이 가능
    • 핵심 비즈니스 로직에만 집중해서 개발 가능
    • 프로젝트마다 다른 관심사를 적용할 때 코드 수정 최소화
    • 원하는 관심사의 유지보수가 수월한 코드로 구성 가능
  • 편리한 MVC 구조

    • 웹 프로그래밍 개발 시 표준적인 방법인 MVC 패턴 사용
  • 트랜잭션의 지원

    • 어노테이션이나 XML 설정을 통해 개발자가 매번 상황에 맞는 코드를 작성할 필요 없도록 함
  • WAS의 종속적이지 않은 개발 환경

의존성 주입 테스트

  • 추가 라이브러리
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-test</artifactId>
    	<version>${org.springframework-version}//스프링버전</version>
    	<scope>test</scope>
    </dependency>
  • 테스트 클래스들
    • DITest2 클래스가 DITest1 클래스를 주입받는 상황
    • DITest1
      ...
      
      @Component
      @Data
      public class DITest1{
      	
      }
    • DITest2
      ...
      
      @Component
      @Data
      public class DITest2{
      	@Setter(onMethod_ = @Autowired)
      	private DITest1 diTest;
      }
  • 사용된 어노테이션
    • @Component
      • 해당 클래스가 스프링에서 관리해야하는 대상임을 표시
    • @Setter
      • 자동으로 해당 필드의 Setter 메소드를 생성
    • @Setter의 onMethod 속성
      • 해당 필드의 Setter 메소드에 @Autowired 어노테이션 추가
    • @Autowired
      • 필요한 의존 객체의 타입에 해당하는 스프링에서 관리되는 객체를 찾아 주입
  • XML을 이용하는 의존성 주입 설정
    • 클래스에서 객체를 생성하고 객체들의 의존성에 대한 처리 작업까지 내부에서 처리
    • root-context.xml 파일에서 NameSpaces 탭 클릭, context 항목 체크
    • root-context.xml 파일 추가 코드
      ...
      <context:component-scan base-package="패키지경로"></context:component-scan>
    • root-context.xml 파일의 Bean Graph 탭에서 설정 확인 가능
  • Java 설정을 이용하는 의존성 주입
    • root-context.xml 파일 대신 설정 자바 파일에서 @ComponentScan 어노테이션을 이용
    • 형태
      ...
      
      @Configuration // 설정 파일임을 나타냄
      @ComponentScan(basePackages= {"패키지명"})
      public class RootConfig{
      	...
      }

의존성 주입에서 root-context.xml이 동작하는 순서

  • 스프링에서 사용하는 메모리 영역인 컨텍스트 생성, ApplicationContext라는 객체가 생성
  • 스프링이 객체를 생성하고 관리하는 객체에 대한 설정을 root-context.xml 파일에서 가져옴
  • root-context.xml에 설정되어 있는 <context:component-scan> 태그를 통해 해당 패키지를 스캔
  • 해당 패키지의 클래스 중 @Component라는 어노테이션이 존재하는 클래스의 인스턴스를 생성
  • A객체에서 B객체 필요하다는 표시인 @Autowired 설정이 되어있으면 A객체에 B객체의 레퍼런스를 주입해서 의존성을 주입함

의존성 주입 테스트 코드

  • 테스트 코드 중요 부분

    패키지 관련 코드...
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration("file:root-context.xml 파일 경로")
    @Log4j2
    public class testClass{
    	@Setter(onMethod_ = { @Autowired })
    	private B b;
    
    	@Test
    	public void testMethod(){
    		assertNotNull(b);
    		
    			log.info(b);
    			...
    		}
    }
    • @RunWith 어노테이션
      • 테스트 코드가 스프링을 실행하는 역할을 할 것이라고 알려줌
      • 스프링은 SpringJUnit4ClassRunner.class로 설정
    • @ContextConfiguration 어노테이션
      • 지정된 클래스나 문자열을 이용해서 필요한 객체들을 스프링 내에 객체로 등록
      • 속성값으로 'classpath:경로', 'file:경로'형태로 사용가능
    • @Log4j2 어노테이션
      • Lombok을 이용해 로그를 기록하는 Logger를 변수로 생성
    • @Autowired 어노테이션
      • 해당 인스턴스 변수가 스프링으로부터 자동으로 할당해 달라는 표시
      • 정상적으로 할당이 가능하면 객체 변수에 해당 타입의 객체를 할당
    • @Test 어노테이션
      • JUnit에서 테스트 대상을 표시하는 어노테이션
    • assertNotNull()
      • 객체 변수가 null이 아니어야만 테스트가 성공한다는 것을 의미

스프링 4.3 이후 단일 생성자의 묵시적 자동 주입

  • 생성자 주입은 생성자를 통해 처리
  • 객체 생성 시 의존성 주입이 필요하므로 좀 더 엄격하게 의존성 주입을 체크하는 장점을 가짐
  • 클래스에 생성자를 정의, 생성자에서 어노테이션 없이 해당 변수 할당
  • 테스트 코드에서 @Setter(onMethod_ = { @Autowired })로 의존성 주입

✏️ 스프링과 Oracle Database 연동

오라클 DB 계정 생성

  • 관리자 계정으로 DB 접속
    // 사용자 계정 생성
    CREATE USER 계정명 IDENTIFIED BY 비밀번호
    DEFAULT TABLESPACE USERS
    TEMPORARY TABLESPACE TEMP;
    
    // 사용자 계정에 권한 부여하기
    GRANT CONNECT, DBA TO 계정명;

프로젝트와 JDBC 연결하기

  • 직접 JDBC 추가하기
    • Sql Developer 폴더의 jdbc > lib 에 있는 ojdbcX.jar파일을 WEB-INF/lib에 복사
  • Maven으로 JDBC 추가하기
    • pom.xml에 해당 JDBC 라이브러리 코드 추가
      <dependency>
      	<groupId>com.oracle.ojdbc</groupId>
      	<artifactId>ojdbc8</artifactId>
      	<version>JDBC 버전</version>
      </dependency>
    • Maven > Update Project 클릭
  • JDBC 테스트 코드
    ...
    
    @Log4j2
    public class JDBCTests{
    	static{
    		try{
    			Class.forName("JDBC 패키지 경로");
    		} catch(Exception e){
    			...
    		}
    	}
    
    	@Test
    	public void testConnection(){
    		try(Connection conn = DriverManager.getConnection(JDBCURL, ID, PWD)){ ...
    	}
    }

커넥션 풀 설정

  • 여러 명의 사용자를 동시에 처리해야 하는 웹 애플리케이션의 경우 데이터베이스 연결을 이용할 때 커넥션 풀을 이용
  • DataSource 인터페이스를 통해 미리 DB와 연결하고 반환하는 구조
  • HikariCP 라이브러리를 이용
  • HikariCP 세팅
    • pom.xml
      <dependency>
      	<groupId>com.zaxxer</groupId>
      	<artifactId>HikariCP</artifactId>
      	<version>HikariCP 버전</version>
      </dependency>
    • root-context.xml
      ...
      <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
      	<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"></property>
      	<property name="jdbcUrl" value="jdbc:oracle:thin:@localhost:1521:XE"></property>
      	<property name="username" value="DB 계정명"></property>
      	<property name="password" value="DB 계정 비밀번호"></property>
      </bean>
      
      <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
      	<constructor-arg ref="hikariConfig" />
      </bean>
      ...

✏️ MyBatis와 스프링 연동

MyBatis란?

  • 쿼리 기반 웹 애플리케이션을 개발할 때 가장 많이 사용되는 SQL Mapper 프레임워크
  • JDBC를 보다 편하게 사용하기 위해 개발

MyBatis 특징

  • SQL문이 코드로부터 완전히 분리
    • Mapper 파일에 SQL 코드를 입력, DAO에서 필요할 때마다 가져와 사용
  • 생산성 증가
    • 작성 코드가 짧아짐
  • 유지보수성 향상
    • Mapper 파일에서 SQL 코드 입력
    • 변경할 때 Mapper 파일만 수정, DAO에 아무런 영향이 없음

MyBatis 장점

  • 자동으로 Connection close() 기능
  • MyBatis 내부적으로 PreparedStatement처리
  • #{prop}와 같이 속성을 지정하면 내부적으로 자동처리
  • 리턴 타입을 지정하는 경우 자동으로 객체 생성 및 ResultSet 처리

MyBatis 라이브러리 추가하기

  • spring-jdbc/spring-tx
    • 데이터베이스 처리와 트랜잭션 처리
    • 추가 안하면 에러 발생
  • mybatis/mybatis-spring
    • MyBatis와 스프링 연동용 라이브러리
  • pom.xml
    <dependency>
    	<groupId>org.mybatis</groupId>
    	<artifactId>mybatis</artifactId>
    	<version>mybatis 버전</version>
    </dependency>
    
    <dependency>
    	<groupId>org.mybatis</groupId>
    	<artifactId>mybatis-spring</artifactId>
    	<version>mybatis-spring 버전</version>
    </dependency>
    
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-tx</artifactId>
    	<version>spring-tx 버전</version>
    </dependency>
    
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-jdbc</artifactId>
    	<version>spring-jdbc 버전</version>
    </dependency>

SQLSessionFactory

  • 데이터베이스와의 연결과 SQL의 실행에 대한 모든 것을 가진 가장 중요한 객체
  • DataSource를 참조하여 MyBatis와 DB 서버를 연동
  • root-context.xml 설정 코드
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    	<property name="dataSource" ref="dataSource"></property>
    </bean>

스프링과의 연동처리

  • Mapper 인터페이스
    • MyBatis 어노테이션을 이용해 SQL을 메소드에 추가
    • 코드
      ...
      
      public interface Mapper{
        // java로 mapper 설정
      	@Select("SELECT문") // getMapper1 메소드에 SELECT문 추가
      	public String getMapper1();
      
      	// xml로 mapper설정
        public String getMapper2();
      }
      
      ...
    • root-context.xml에 Mapper 설정
      • root-context.xml 파일 하단 Namespaces에서 mybatis-spring 추가
      • 추가 코드
        <mybatis-spring:scan base-package="Mapper 패키지 경로" />
    • XML 파일로 Mapper 설정
      • src/main/resources에 Mapper를 설정할 XML 파일 생성
      • mapper 설정 XML 파일
        ...
        <mapper namespace="mapper 패키지 경로">
        	// insert, update, delete, select 태그가 존재
        	<select id="메소드명" resultType="반환타입">
        		SELECT 문 
        	</select>
        </mapper>
    • Mapper 테스트
      ...
      
      @RunWith(SpringJUnit4ClassRunner.class)
      @ContextConfiguration("file:root-context.xml 경로" | classes = { RootConfig 패키지 경로 })
      @Log4j2
      public class mapperTests{
      	@Setter(onMethod_ = @Autowired)
      	private Mapper mapper;
      
       	@Test
      	public void getMapperTest1(){
      		log.info(mapper.getClass().getName());
      		log.info(mapper.getMapper1());
      	}
      
       	@Test
      	public void getMapperTest2(){
      		log.info("getMapper2");
      		log.info(mapper.getMapper2());
      	}
      }

log4jdbc-log4j2 설정

  • JDBC는 PreparedStatement를 이용해 SQL을 처리하는데 SQL문에 전달되는 파라미터는 '?'로 지환되서 처리됨.
  • SQL문이 오류가 없는지 확인이 어렵고, 실행된 SQL문의 내용을 정확히 확인하기 여러움
  • 위 문제들을 해결하기 위해 log4jdbc-log4j2 라이브러리를 사용하여 SQL 로그를 제대로 표시
  • pom.xml 추가 설정
    <dependency>
    	<groupId>org.bgee.log4jdbc-log4j2</groupId>
    	<artifactId>log4jdbc-log4j2-jdbc4</artifactId>
    	<version>log4jdbc-log4j2-jdbc4 버전</version>
    </dependency>
  • log4jdbc.log4j2.properties 파일 추가
    log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
  • root-context.xml 수정
    ...
    
    <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
    	<property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property>
    	<property name="jdbcUrl" value="jdbc:log4jdbc:oracle:thin:@localhost:1521:XE"></property>
    
    ...

로그 레벨 설정

  • 테스트 코드를 실행하면 상당히 많은 양의 로그가 출력, 로그 레벨을 설정 출력 로그를 줄임
  • log4j.xml 삭제
  • log4j2.xml 생성 후 설정
    <!-- Appender, Layout 설정 -->
    	<Appenders>
    		<Console name="console" target="SYSTEM_OUT">
    			<PatternLayout pattern=" %-5level %c(%M:%L) - %m%n" />
    		</Console>
    	</Appenders>
    	<!-- Logger 설정 -->
    	<Loggers>
    		<Root level="INFO">
    			<AppenderRef ref="console" />
    		</Root>
    		<Logger name="org.zerock" level="INFO" additivity="false">
    			<AppenderRef ref="console" />
    		</Logger>
    		<Logger name="org.springframework" level="DEBUG" additivity="false">
    			<AppenderRef ref="console" />
    		</Logger>
    	</Loggers>

✏️ 스프링 MVC 설정

스프링 MVC 기본 사상

  • 스프링 개발자 코드 영역 <-> Spring MVC <-> Servlet/JSP
  • 스프링 MVC
    • 개발자는 직접적으로 Servlet/JSP의 API를 사용할 필요성이 줄어듬
    • 스프링은 중간에 연결 역할을 하기 때문에 이러한 코드를 작성하지 않고도 원하는 기능을 구현할 수 있음
  • 예전 스프링 MVC는 특정한 클래스를 상속하거나 인터페이스를 구현하는 형태로 개발
  • 스프링 2.5버전부터 어노테이션와 XML 등의 설정만으로 개발이 가능

모델2와 스프링 MVC

  • 스프링 MVC는 내부적으로 Servlet API를 활용
  • 모델 2
    • 간단하게 로직과 화면을 분리하는 스타일의 개발 방식
    • MVC 구조 사용
      모델2
    • Controller
      • 데이터를 처리하는 존재를 이용해서 데이터를 처리하고 Response 할 때 필요한 데이터를 view쪽으로 전달
      • View를 교체하더라도 사용자가 호출하는 URL자체에 변화가 없게 만듬
    • Model
      • 실질적으로 데이터를 처리
    • View
      • 사용자에게 실질적으로 보여지는 페이지
  • 스프링 MVC
    • 모델 2 방식으로 처리되는 구조
      스프링 MVC
    • DispatcherServlet
      • 사용자의 요청을 전달받아 요청에 맞는 컨트롤러가 리턴한 결과값을 View에 전달하여 알 맞은 응답을 반환
    • HandlerMapping
      • 요청의 처리를 담당하는 컨트롤러를 찾기 위해 존재
      • 적절한 컨트롤러를 찾았으면 HandlerAdapter를 이용해 해당 컨트롤러를 동작
    • Controller
      • 실제 요청을 처리하는 로직을 작성
      • View로 전달해야 하는 데이터는 Model 객체에 담아서 전달
      • ModelAndView : 컨트롤러의 처리 결과를 생성할 뷰를 결정
    • ViewResolver
      • Controller가 반환한 결과를 어떤 View를 통해 처리하는 것이 좋을지 해석하는 역할
      • servlet-context.xml의 InternalResourceViewResolver를 통해 설정
        <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        	<beans:property name="prefix" value="경로" />
        	<beans:property name="suffix" value=".확장자" />
        </beans:bean>
    • View
      • 실제로 응답 보내야 하는 데이터를 JSP등을 이용해 생성하는 역할
      • 응답은 DispatcherServlet을 통해 전송
  • 위 그림을 보면 모든 요청을 DispatcherServlet을 통해 설계, 이런 방식을 Front-Controller 패턴이라고함.
  • Front-Controller 패턴을 이용하면 전체흐름을 강제로 제한할 수 있음

스프링 MVC의 Controller

  • Controller의 특징
    • HttpServletRequest, HttpServletResponse를 거의 사용할 필요 없이 필요한 기능 구현
    • 다양한 타입의 파라미터 처리, 다양한 타입의 리턴 타입 사용 가능
    • 전송 방식에 대한 처리를 어노테이션으로 처리 가능
    • 상속/인터페이스 방식 대신 어노테이션만으로토 필요한 설정 가능
  • @Controller 어노테이션과 @RequestMapping 어노테이션
    • 예제 클래스
      ...
      
      @Controller
      @RequestMapping("/경로/*")
      public class Controller{
      	...
      }
    • @Controller 어노테이션
      • Model 객체를 만들어 데이터를 담고 View를 반환하는 것
      • 해당 클래스는 자동으로 스프링의 객체로 등록
      • servlet-context.xml에 <context:component-scan> 태그를 이용해 지정된 패키지를 조사하도록 설정, 해당 패키지를 조사하며 스프링 객체 설정에 사용되는 어노테이션을 가진 클래스를 파악하고 필요하면 이를 객체로 생성해서 관리
        <context:component-scan base-package="패키지 경로" />
      • 스프링에서 관리되면 화면상 클래스 옆에 작게 s 모양 아이콘이 추가
      • 추가적인 속성을 지정할 수 없음
    • @RequestMapping 어노테이션
      • 현재 클래스의 모든 메소드들의 기본적인 URL 경로
      • 클래스 선언과 메소드 선언에 사용 가능
      • @GetMapping, @PostMapping 으로 축약형 표현이 존재
    • @RequestMapping 어노테이션 속성
      • value 속성
        • URL 패턴을 지정하는 속성
          // 기본 형태
          @RequestMapping(value="/경로")
          
          // value=를 생략한 경우
          @RequestMapping("/경로")
          
          // URL이 여러개인 경우
          @RequestMapping({"/경로1", "/경로2"})
          @RequestMapping(value = {"/경로1", "/경로2"})
      • method 속성
        • HTTP 요청 메소드값이 일치해야 매핑이 이루어짐
        • URL만 설정 시 get/post 둘다 사용 가능
        • GET, POST, PUT, DELETE 등 7개의 HTTP 메서드가 정의
          // GET/POST 메소드를 설정한 경우
          @RequestMapping(value="/경로" method=RequestMethod.GET | POST)
          public static 메소드명(...
          
          // static import로 RequestMethod 라이브러리를 추가
          import static org.springframework.web.bind.annotation.RequestMethod.*;
          @RequestMapping(value="/경로" method=GET | POST)
          
          // Pathvariable로 파라미터 전송
          @RequestMapping("/경로/{ 속성명 }")
          public ModelAndView 메소드명(@PathVariable int 파라미터1){
          	ModelAndView view = new ModelAndView();
          	view.setViewname("View명");
          	view.addObject("속성명", 파라미터1)
          	return view;
          }
      • params
        • 요청 파라미터와 값으로 구분 가능
        • String 배열로 여러개를 지정 가능
          // 파라미터명=파라미터값 : 파라미터명이 파라미터값일 때 호출
          // 파라미터명!=파라미터값 : 파라미터명이 파라미터값이 아닐 때 호출
          // 파라미터명 : 파라미터에 존재할 때 호출
          // !파라미터명 : 파라미터에 존재하지 않을 때 호출
          @RequestMapping(value="/경로", params="파라미터명=파라미터값") 
          
          // 파라미터가 여러개인 경우
          @RequestMapping(value="/경로", params={ "파라미터명1=파라미터값1", "파라미터명2", ...}) 
      • headers 속성
        • 헤더 값으로 구분
          @RequsetMapping(value="/경로", headers="해더")

Controller의 파라미터 수집

  • Controller는 파라미터 타입에 따라 자동으로 변환하는 방식으로 파라미터를 수집
  • @RequestParam
    • 파라미터로 사용된 변수의 이름과 전달되는 파라미터의 이름이 다른 경우에 사용
    • 컨트롤러에서 Requestparam으로 파라미터 값을 넘겨받을 때 사용하는 어노테이션 
  • 기본 자료형, 문자열
    • 파라미터 타입만 맞게 선언해주는 방식
      @GetMapping("경로")
      public String 메소드명(@RequestParam("파라미터명1") String 파라미터명1, @RequestParam("파라미터명2") int 파라미터명2){
      	log.info("파라미터1 : " + 파라미터1);
      	log.info("파라미터2 : " + 파라미터2);
      	
      	return "이동할경로";
      }
  • 리스트, 배열
    • 파라미터가 여러 개 전달되는 경우
      // 리스트
      @GetMapping("경로")
      public String 메소드명(@RequestParam("파라미터명") ArrayList<String> 파라미터명){
      	log.info("파라미터 : " + 파라미터);
      	
      	return "이동할경로";
      }
      
      // 배열
      @GetMapping("경로")
      public String 메소드명(@RequestParam("파라미터명") String[] 파라미터명){
      	log.info("파라미터 : " + Arrays.toString(파라미터));
      	
      	return "이동할경로";
      }
  • 객체 리스트
    • 빈 객체를 데이터로 받을 때 사용
      @GetMapping("경로")
      public String 메소드명(Bean클래스명 파라미터명){
      	log.info("파라미터 : " + 파라미터);
      	
      	return "이동할경로";
      }
  • @InitBinder 어노테이션
    • 파라미터를 수집을 할 때 변환이 가능한 데이터는 자동으로 변환되지만 Date와 같은 데이터 타입은 직접 바인딩을 해줘야함
    • 바인딩에 실패하면 브라우저에서 400에러 발생
      // Controller의 일부분
      @InitBinder
      public void initBinder(WebDataBinder binder){
      	// 예시로 Date타입 변환
      	SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd");
      	binder.registerCustomEditor(java.util.Date.class, new CustomDateEditer(dateFormat, false));
      }
  • @DateTimeFormat 어노테이션
    • @InitBinder를 이용해서 날짜를 변환 가능하지만, 파라미터로 사용되는 인스턴스 변수에 @DateTimeFormat 어노테이션을 이용해 변환 가능
      @Data
      public class 클래스명{
      	private String title;
      
      	@DateTimeFormat(pattern="yyyy-mm-dd")
      	private Date date;
      }
  • Model 데이터 전달자
    • Controller 메소드를 작성할 때 매개변수로 Model 타입을 사용 가능
    • Model 객체
      • JSP에 컨트롤러에서 생성된 데이터를 담아서 전달하는 역할
    • 모델 2 방식의 request.setAttribute()와 비슷한 역할
    • 주로 Controller에 전달된 데이터를 이용해서 추가적인 데이터를 가져와야하는 상황에서 사용
      • 리스트 페이지 번호를 파라미터로 전달, 실제 데이터를 View로 전달해야하는 경우
      • 파라미터들에 대한 처리 후 결과를 전달해야 하는 경우
        // Controller의 일부
        public String 메소드명(Model model){
        	model.addAttribute("속성명", 속성값);
        
        	return "경로";
        }
  • ModelAttribute 어노테이션
    • 강제로 전달받은 파라미터를 Model에 담아서 전달하도록 할 때 필요한 어노테이션
    • 파라미터는 타입에 관계없이 무조건 Model에 담아서 전달, 파라미터로 전달된 데이터를 다시 화면에서 사용해야 할 경우에 사용
      // Controller의 일부
      // 데이터를 웹 페이지에 전달할 때 빈객체 파라미터는 전달됨
      // 하지만 다른 파라미터는 해당 어노테이션을 사용하지 않으면 웹 페이지에 전달되지 않음.
      public String 메소드명(빈객체 파라미터1,@ModelAttribute("속성명") 파라미터2){
      	log.info("파라미터1 : " + 파라미터1);
      	log.info("파라미터2 : " + 파라미터2);
      
      	return "경로";
      }
  • RedirectAttributes
    • 일회성으로 데이터를 전달하는 용도로 사용
    • response.sendRedirect()를 사용할 때와 동일한 용도로 사용
      // Controller의 일부
      public String 메소드명( ... ){
      	...
      
      	// addFlashAttribute를 사용해 화면에 한 번만 사용하고 다음에 
      	// 사용되지 않는 데이터를 전달하기 위해 사용
      	rttr.addFlashAttribute("데이터명1", 데이터명1)
      	rttr.addFlashAttribute("데이터명3", 데이터명2)
      	...
      	
      	return "redirect:/";
      }

Controller 리턴 타입

  • servlet-context.xml에서 파일 경로 설정
    <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    	<beans:property name="prefix" value="/WEB-INF/views/" />
    	<beans:property name="suffix" value=".jsp" />
    </beans:bean>
  • 리턴 타입에 따른 기능
    • String 타입
      • JSP를 이용하는 경우 JSP 파일의 경로와 파일이름을 나타내기 위해 사용
      • 상황에 따라 다른 화면을 보여줘야할 때 많이 사용
    • void 타입
      • 호출하는 URL과 동일한 이름의 JSP를 의미
    • VO, DTO 타입
      • 주로 JSON 타입의 데이터를 만들어서 반환하는 용도로 사용
      • jackson-databind 라이브러리 이용하지 않으면 에러 발생
    • ResponseEntity 타입
      • response 할 때 Http 헤더 정보와 내용을 가공하는 용도로 사용
    • Model, ModelAndView
      • Model로 데이터를 반환하거나 화면까지 같이 지정하는 경우에 사용(최근에는 거의 사용하지 않음)
    • HttpHeaders
      • 응답에 내용 없이 Http 헤더 메시지만 전달하는 용도로 사용
  • 파일 업로드 처리
    • 추가 라이브러리
      // pom.xml
      <dependency>
      	<groupId>commons-fileupload</groupId>
      	<artifactId>commons-fileupload</artifactId>
      	<version>commons-fileupload 버전</version>
      </dependency>
    • servlet-context.xml 설정
      <beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
      	<bean:property name="defaultEncoding" value="utf-8"></beans:property>
      	<bean:property name="maxUploadSize" value="request로 전달될 수 있는 최대 크기"></beans:property>
      	<bean:property name="maxUploadSizePerFile" value="하나의 파일 최대 크기"></beans:property>
      	<bean:property name="uploadTimpDir" value="임시파일 저장 경로"></beans:property>
      	<bean:property name="maxInMemorySize" value="메모리상에서 유지하는 최대크기"></beans:property>
      </beans:bean>

Controller 예외처리

  • 예외 처리 클래스
    @ControllerAdvice
    @log4j2
    public class CommonExceptionAdvice {
    
    	@ExceptionHandler(예외처리 클래스)
    	public String except(Exception ex, Model model){
    		log.error("Exception........" + ex.getMessage());
    		model.addAttribute("exception", ex);
    		log.error(model);
    		return "error_page";
    	}
    }
  • @ControllerAdvice 어노테이션
    • 해당 객체가 스프링의 컨트롤러에서 발생하는 예외를 처리하는 존재임을 명시하는 용도
  • @ExceptionHandler 어노테이션
    • 해당 메소드가 ()에 들아가는 예외 타입을 처리한다는 것을 의미

✏️ 스프링 MVC 프로젝트의 기본 구성

3-Tier 계층

3-Tier

  • Presentation Tier
    • 화면에보여주는 기술을 사용하는 영역
    • Servlet/JSP나 스프링 MVC가 담당하는 영역
  • Business Tier
    • 순수한 비즈니스 로직을 담고 있는 영역
    • 고객이 원하는 요구 사항을 반영하는 계층
  • Persistence Tier
    • 데이터를 어떤 방식으로 보관/사용하는가에 대한 설계가 들어가는 계층

한글 문제와 UTF-8 필터 처리

  • 브라우저에서 한글이 깨져서 전동되는지 확인
  • 스프링 MVC 쪽에서 한글을 처리하는 필터를 등록
  • web.xml 추가
    <filter>
    	<filter-name>encoding</filter-name>
    	<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    	<init-param>
    		<param-name>encoding</param-name>
    		<param-class>UTF-8</param-class>
    	</init-param>
    </filter>
    
    <filter-mapping>
    	<filter-name>encoding</filter-name>
    	<servlet-name>appServlet</servlet-name>
    </filter-mapping>

오라클 데이터베이스 페이징 처리

  • order by의 문제

    • 데이터 양이 많을 때 정렬을 하게 되면 엄청나게 많은 시간과 리소스를 소모
    • 데이터가 적은 경우, 정렬을 빠르게 할 수 있는 방법이 있는 경우가 아니면 사용X
  • 실행 계획

    • SQL 구문을 데이터베이스에서 어떻게 처리할 것인지에 대한 것
    • SQL 파싱 단계
      • SQL 구문에 오류가 있는지 SQL을 실행해야하는 대상 객체(테이블, 제약 조건, 권한 등)가 존재하는지 검사
    • SQL 최적화 단계
      • SQL이 실행되는데 필요한 비용 계산하는 작업
    • SQL 실행 단계
      • 실행 계획을 통해 메모리상에서 데이터를 읽거나 물리적인 공간에서 데이터를 로딩하는 등의 작업
    • 실행 계획 보는 법
      실행 계획
      • '실행 계획은 안쪽에서 바깥쪽으로, 위에서 아래로 보면 된다.' 라고 책에 나오는데 이해하기가 어렵다.
      • 안쪽에서 바깥쪽으로 : OBJECT_NAME -> OPTIONS -> ...
      • 위에서 아래로 : TB1_BOARD -> PK_BOARD -> ...
  • 인덱스를 이용한 데이터 정렬

    • 인덱스란?
      • 테이블의 컬럼을 색인화하여 검색할 때 색인을 이용해 검색하여 검색 속도를 개선
      • 인덱스를 이용하면 테이블의 칼럼이 이미 정렬되어 있어 기존 SQL 구문 실행 시 정렬되는 과정을 생략할 수 있어 검색 속도가 빠름.
    • 인덱스를 사용하는 이유
      • where 구문과 일치하는 열을 빨리 찾기 위해
      • 특정 열을 고려 대상에서 빨리 없애 버리기 위해
      • 조인을 실행 할 때 다른 테이블에서 열을 추출하기 위해
      • 특정 인덱스된 칼럼을 위한 MIN() 또는 MAX() 값을 찾기 위해
      • 정렬 및 그룹화를 하기 위해
      • 쿼리를 최적화하기 위해
    • 인덱스의 장점
      • 키 값을 기초로 테이블에서 검색과 정렬 속도를 향상
      • 질의나 보고서에서 그룹화 작업 속도 향상
      • 테이블 행의 고유성을 강화
      • 테이블의 기본 키는 자동으로 인덱스됨.
    • 인덱스의 단점
      • 인덱스 생성시 .mdb 파일의 크기가 증가
      • 한 페이지를 동시에 수정할 수 있는 병행성이 줄어듬
      • 데이터를 추가하거나 삭제, 업데이트할 때 성능에 영향을 미침
      • 인덱스를 생성하는데 많은 시간이 소요
      • 인덱스가 데이터베이스 공간을 차지해 추가적인 공간이 필요
  • 힌트를 이용한 데이터 정렬

    • 힌트란?
      • SQL 문을 어떻게 처리하는지 사용자가 직접 최적의 실행 경로를 작성해 주는 것
      • SQL 튜닝의 핵심 부분, 일종의 지시 구문
    • 힌트 형태
      • /*+ (힌트명) */
    • FULL 힌트
      • 테이블 전체를 스캔할 것
      • 데이터가 많을 때 상당히 느리게 실행
        select /*+ FULL(테이블명) */ * from 테이블 order by 기준칼럼 desc;
    • INDEX_ASC/INDEX_DESC 힌트
      • 목록 페이지에서 가장 많이 사용하는 힌트
      • 인덱스 탐색을 어떻게 할 것인지 지정
      • 해당 힌트를 사용하여 정렬 과정을 생략
        select /*+ INDEX_ASC(테이블명 인덱스명) */ * from 테이블 where 조건;
  • ROWNUM

    • SQL이 실행된 결과에 넘버링을 붙여줌
    • 실제 데이터가 아니라 테이블에서 데이터를 추출한 후에 처리되는 변수로 매번 값이 달라질수 있음
    • 정렬 과정에서 ROWNUM은 변경되지 않음

Mybatis의 동적 SQL

  • <if>
    • 특정 조건이 true가 되었을 때 포함된 SQL을 사용하고자할 때 작성
      <if test="조건">
      	SQL문
      </if>
  • <choose>
    • 여러 상황들 중 하나의 상황에서만 동작
    • switch문과 비슷함
      <choose>
      	<when test="조건">
      		SQL문
      	</when>
      	<when test="조건">
      		SQL문
      	</when>
      	...
      	<otherwise>
      		SQL문
      	</otherwise>
      </choose>
  • <where>
    • 태그 안쪽에 SQL이 생성될 때는 WHERE 구문이 붙고, 그렇지 않는 경우에 생성하지 않음
      select * from 테이블명
      <where>
      	where문
      </where>
  • <trim>
    • 하위에서 만들어지는 SQL문을 조사하여 앞 쪽에 추가적인 SQL문을 넣을 수 있음
    • prefix, suffix, prefixOverrides, suffixOverrides 속성을 지정 가능
  • <foreach>
    • List, 배열, 맵 등을 이용해서 루프를 처리
    • IN 조건을 많이 사용하지만, 복잡한 WHERE 조건을 만들때에도 사용
      select * from 테이블명
      <foreach item="변수명" index="" collection="콜랙션 타입 종류">
      	where문
      </foreach>

✏️ REST 방식과 Ajax로 댓글 처리

REST 방식

  • REST 란?
    • Representational State Transfer의 약어로 하나의 URI는 하나의 고유한 리소스를 대표하도록 설계된다는 개념에 전송방식을 결합해서 원하는 작업을 지정
  • 스프링에서 지원하는 어노테이션
    • @RestController
      • Controller가 REST 방식을 처리하기 위한 것임을 명시
      • 메소드의 리턴 타입으로 사용자가 정의한 클래스 타입을 사용, 그 데이터를 JSON이나 XML로 자동으로 처리
    • @ResponseBody
      • 일반적인 JSP와 같은 뷰로 전달되는게 아니라 데이터 자체를 전달하기 위한 용도
    • @PathVariable
      • URL 경로에 있는 값을 파라미터로 추출하려고 할 때 사용
    • @CrossOrigin
      • Ajax의 크로스 도메인 문제를 해결해주는 어노테이션
    • @RequestBody
      • JSON 데이터를 원하는 타입으로 바인딩 처리
  • @RestController의 반환 타입
    • 단순 문자열 반환
      • @Controller의 반환과 달리 순수한 데이터를 반환하는 형태로 다양한 포맷의 데이터를 전송 가능
      • 주로 일반 문자열, JSON, XML 등을 사용
        @RestController
        @RequestMapping("/경로")
        @Log4j2
        public class Controller{
        	@GetMapping(value="/세부경로", produces="text/plain; charset=UTF-8")
        	public String Method(){
        		log.info("MIME TYPE : " + MediaType.TEXT_PLAIN_VALUE);
        		
        		return "데이터";
        	}
        }
    • 객체 반환
      • produces 속성을 지정하여 데이터를 JSON이나 XML으로 변환]
      • APPLICATION_JSON_UTF8_VALUE는 스프링 5.2버전부터 Deprecated, 5.2버전 이후는 APPLICATION_JSON_VALUE를 사용
        @GetMapping(value="/경로", produces={MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_XML_VALUE})
        public 빈객체 method(){
        	return new 빈객체(...);
        }
    • 컬렉션 타입의 객체 반환
      • 여러 데이터를 한 번에 전송하기 위해 배열이나 리스트, 맵 타입의 객체들을 전송
        @GetMapping(value="/경로")
        public List<SampleVO> method(){
        	// 내부적으로 1~10까지 루프를 처리, 빈객체를 만들어 리스트로 만듬
        	return IntSteam.range(1,10).mapToObj(i -> new 빈객체(...)).collect(Collectors.toList());
        	
        	// MAP 타입을 리턴
        	Map<String, 빈객체> map = new HashMap<>();
        	map.put("First", new 빈객체(...));
        	
        	return map;
        }
    • ResponseEntity 타입
      • 데이터와 함께 HTTP 헤드의 상태 메시지 등을 같이 전달하는 용도로 사용
      • HTTP 상태 코드와 에러 메시지 등을 함께 데이터를 전달할 수 있기 때문에 받는 입장에서는 확실하게 결과를 알 수 있음.
        @GetMapping(value="/경로" params={"height", "weight"})
        public ResponseEntity<sampleVO> check(Double height, Double weight){
        	빈객체 vo = new 빈객체(...);
        	
        	ResponseEntity<빈객체>result = null
        	if(height < 150){
        		result = ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(vo);
          }
        	else{
        		result = ResponseEntity.status(HTTPStatus.OK).body(vo);
        	}
        }
  • @RestController 파라미터
    • @PathVariable
      • 스프링 MVC에서 URL 경로의 일부를 파라미터로 사용할 때 사용
      • 예를 들어, URL경로가 http:localhost:8080/sample/{sno}/page/{pno}일 때 {}로 처리된 부분은 컨트롤러의 메소드에서 변수로 처리 가능
      • {}를 이용해 변수명을 지정, @PathVariable을 이용해서 지정된 이름의 변숫값을 얻을 수 있음
      • 값을 얻을 때 int,double과 같은 기본 자료형은 사용할 수 없음.
        @GetMapping("/경로/{세부경로1}/{세부경로2}")
        public String[] method(@PathVariable("세부경로1") String 세부경로1, 
        						@PathVariable("세부경로2") Integer 세부경로2){
        	return new String[] {"속성1:"+세부경로1, "속성2:"+세부경로2};
        }
  • @RequestBody
    • 전달된 요청의 내용을 이용해서 해당 파라미터의 타입으로 변환을 요구
    • 내부적으로 HttpMessageConverter 타입의 객체들을 이용해서 다양한 포맷의 입력 데이터를 변환
    • 요청한 내용을 처리하기 때문에 일반적인 파라미터 전달 방식을 사용할 수 없음
      @PostMapping("/ticket")
      public 빈객체 method(@RequestBody 빈객체 파라미터명){
      	log.info("convert...........ticket" + 파라미터명);
      	
      	return 파라미터명;
      }

✏️ AOP와 트랜잭션

AOP 패러다임

  • AOP란?
    • Aspect Oriented Programming의 약자로 관점 지향 프로그래밍
    • 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 그 관점을 기준으로 각각 모듈화하겠다는 것
    • 관심사를 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것
  • AOP 주요 개념
    • Aspect
      • 위에서 설명한 흩어진 관심사를 모듈화 한 것.
      • 부가 기능을 모듈화
    • Target
      • Aspect를 적용하는 곳
      • 개발자가 작성한 핵심 비즈니스 로직을 가지는 객체
    • Advice
      • 실직적으로 어떤 일을 해야할 지에 대한 것
      • 실질적인 부가기능을 담은 구현체
      • 동작 위치에 따른 구분
        • Before Advice : Target의 JoinPoint를 호출하기 전에 실행되는 코드, 코드 실행 자체에 관여할 수 없음
        • After Returning Advice : 모든 실행이 정상적으로 이루어진 후에 동작하는 코드
        • After Throwing Advice : 예외가 발생한 뒤에 동작하는 코드
        • After Advice : 정상적으로 실행되거나 예외가 발생했을 때 구분없이 실행되는 코드
        • Around Advice : 메서드의 실행 자체를 제어할 수 있는 가장 강력한 코드, 직접 호출/경과/예외 처리 가능
    • JointPoint
      • Advice가 적용될 위치
      • 메소드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능
      • Target 객체가 가진 메소드
    • PointCut
      • JointPoint의 상세한 스펙을 정의한 것
      • 구체적으로 Advice가 실행될 지점을 정할 수 있음
      • 관심사와 비즈니스 로직이 결합되는 지점을 결정하는 것
      • 자주 사용되는 설정
        • execution : 메소드를 기준
        • within : 특정 타입을 기준
        • this : 주어진 인터페이스를 구현한 객체를 기준
        • args : 특정한 파라미터를 가지는 대상들만
        • @annotaion : 특정한 어노테이션이 적용된 대상들만
    • Proxy
      • Target을 전체적으로 감싸고 있는 존재
      • 내부적으로 Target을 호출, 필요한 관심사들을 거쳐 Target을 자동/수동으로 호출
      • 대부분 자동으로 생성되는 방식을 이용
  • AOP 특징
    • 중복을 줄여서 적은 코드 수정으로 전체 변경을 할수 있게하는 것이 목적
    • 핵심관점(업무로직) + 횡단관점(트랜잭션/로그/보안/인증 처리 등) 관심사 분리
  • AOP 장점
    • 중복 코드 제거
    • 효율적인 유지보수
    • 높은 생산성
    • 재활용성 극대화
    • 변화 수용 용이
  • Aspect에서 사용하는 어노테이션
    • @Before 어노테이션
      • 지정한 패턴에 해당하는 메소드가 실행되기 전에, interceptor와 같이 동작하는 것을 의미
      • 메소드 반환 값이 void일 때 사용
    • @After 어노테이션
      • 지정한 패턴에 해당하는 메소드가 실행된 후 동작
      • 메소드 반환 값이 void일 때 사용
    • @Around 어노테이션
      • 지정된 패턴에 해당하는 메소드의 실행되지 전/후 모두 동작
      • 메소드 반환 값이 Object
    • @within
      • 특정 어노테이션 타입을 갖는 객체에 대해 aop를 지정하기 위해 사용
      • 경로가 아니라 AOP를 적용할 메소드가 있는 클래스 타입을 지정
      • 형태
        @within(어노테이션 타입)
    • @target
      • 특정 타입의 어노테이션이 붙어있는 객체를 지정 가능
      • 런타임 때 객체가 일치하는지 확인, retention이 Runtime이어야함.
      • 형태
        @target(어노테이션 타입)
    • @annotation
      • 특정 어노테이션이 붙은 객체에 대해 AOP를 적용
      • @target과 @within보터 더 넓은 범위로 사용할 수 있는 것
      • 형태
        @annotation(어노테이션 타입)
  • pointcut 표현식
    • 와일드 카드를 이용해 작성
    • * : 모든 것
    • .. : 0개 이상
  • execution와 within
    • execution
      • 특정 메소드를 지정하는 패턴을 작성할 수 있는 방법
      • 특정 메소드까지의 패턴을 딱 지정하는 표현식
      • 형태
        @AOP어노테이션("execution([접근제어자] 반환타입 패키지경로.클래스.메소드(인자))")
    • whitin
      • 특정 클래스 안에 있는 메소드를 모두 지정하는 패턴을 작성할 수 있는 방법
      • 클래스명까지 저정 가능
      • 형태
        @AOP어노테이션("whitin(패키지경로.클래스)")

트랜잭션 관리

  • 트랜잭션이란?
    • 완전히 성공하거나 완전히 실패하는 일련의 논리적 작업단위
  • 트랜잭션의 특징
    • 일관성(Atomicity)
      • 하나의 트랜잭션은 모두 하나의 단위로 처리되어야 함
    • 일관성(Consistency)
      • 트랜잭션이 성공했다면 데이터베이스의 모든 데이터는 일관성을 유지
    • 격리(Isolation)
      • 트랜잭션으로 처리되는 중간에 외부에서의 간섭은 없어야함
    • 영속성(Durability)
      • 트랜잭션이 성공적으로 처리되면 그 결과는 영속성으로 보관
  • 데이터베이스 설계와 트랜잭션
    • 정규화
      • 테이블 간에 중복된 데이터를 허용하지 않는다는 것
      • 무결성을 유지할 수 있으며, DB의 저장 용량을 줄일 수 있음
      • 정규화 진행 원칙
        1. 시간이 흐르면 변경되는 데이터를 칼럼으로 기록X
        2. 계산이 가능한 데이터를 칼럼으로 기록X
        3. 누구에게나 정해진 값을 이용하는 경우 데이터베이스에서 취급X
      • 정규화를 진행하면 테이블은 더 간결, 쿼리 등은 조인, 서브쿼리를 이용해야되서 복잡해짐
      • 정규화 정리 블로그
    • 반정규화
      • 시스템 성능 향상, 개발 및 운영의 편의성 등을 위해 정규화된 데이터 모델을 통합, 중복, 분리하는 과정으로 의도적으로 정규화원칙을 위배하는 행위
      • 반정규화 방법으로 테이블 통합, 테이블 분할, 중복 테이블 추가, 중복 속성 추가 등이 있음
      • 반정규화 정리 블로그
  • @Transactional 어노테이션
    • 클래스나 메소드에 붙여 해당 범위 내 메소드가 트랜잭션이 되도록 보장
    • 선언적 트랜잭션이라고도 함
    • 직접 객체를 만들 필요 없이 선언만으로도 관리가 용이
    • 전파(Propagation) 속성
      • PROPAGATION_MADATORY : 작업은 반드시 특정한 트랜잭션이 존재한 상태에서만 가능
      • PROPAGATION_NESTED : 기존에 트랙잭션이 있는 경우, 포함되서 실행
      • PROPAGATION_NEVER : 트랜잭션 상황하에 실행되면 예외 발생
      • PROPAGATION_NOT_SUPPORTED : 트랜잭션이 있는 경우엔 트랜잭션이 끝날 때까지 보류된 후 실행
      • PROPAGATION_REQUIRED : 트랜잭션이 있으면 그 상황에서 실행, 없으면 새로운 트랜잭션 실행(기본)
      • PROPAGATION_REQUIRED_NEW : 대상은 자신만의 고유한 트랜잭션으로 실행
      • PROPAGATION_SUPPORTS : 트랜잭션을 필요로 하지 않으나, 트랜잭션 상황하에 있다면 포함되어서 실행
    • 격리 레벨
      • DEFAULT : DB설정, 기본 격리 수준
      • SERIALIZABLE : 가장 높은 격리, 성능 저하 우려있음
      • READ_UNCOMMITED : 커밋되지 않은 데이터에 대한 읽기를 허용
      • READ_COMMITED : 커밋된 데이터에 대해 읽기 허용
      • REPEATEABLE_RED : 동일 필드에 대해 다중 접근 시 모두 동일한 결과 보장
    • Read-only 속성
      • true인 경우 insert, update, delete 실행 시 예외 발생, false가 기본 설정
    • Rollback-for-예외명
      • 특정 예외가 발생 시 강제로 Rollback
    • No-rollback-for-예외명
      • 특정 예외의 발생시에는 Rollback 처리되지 않음
    • @Transactional 적용 순서
      • 메소드의 @Transactional 설정이 가장 우선
      • 클래스의 @Transactional 설정이 메소드보다 우선순위가 낮음
      • 인터페이스의 @Transactional 설정이 가장 낮은 우선순위

✏️ 파일 업로드 처리

파일 업로드 방식

  • 첨부파일 처리관련 라이브러리
    • cos.jar : 2002년 이후 개발이 종료되어 더 이상 사용하는 것을 권장하지 않음.
    • commons-fileupload : 가장 일반적으로 많이 활용되고, 서블릿 스펙 3.0 이전에도 사용 가능
    • 서블릿 3.0 이상 : 3.0버전 이상부터는 자체적인 파일 업로드 처리가 API 상에서 지원
  • Web.xml에 파일관련 설정
    • WAS(Tomcat) 설정
      <servlet>
      	...
      
      	<multipart-config>
      		<location>파일 저장 경로</location>
      		<max-file-size>최대 파일 크기</max-file-size>
      		<max-request-size>최대 요청 크기</max-request-size>
      		<file-size-threshold>파일이 메모리에 기록되는 임계값</file-size-threshold>
      	</multipart-config>
      </servlet>
  • servlet-context.xml 설정
    • 파일 업로드 처리를 위해 MultipartResolver를 이용
      ...
      
      <beans:bean id="multipartResolver" 
      		class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
      </beans:bean>
  • <form> 태그를 이용
    • 브라우저의 제한이 없어야하는 경우 사용
    • 페이지 이동과 동시에 첨부파일을 업로드하는 방식
    • <iframe>을 이용해서 화면 이동 없이 첨부파일을 처리
    • form태그를 이용한 파일업로드 예제 코드
      • 업로드 버튼이 눌럿을 때 404에러가 발생하는데, 이 에러가 발생하는 것이 정상적임
      • 해당 에러가 발생했을 때 업로드 폴더 확인 필수!
  • Ajax 통신를 이용
    • 첨부파일을 별도로 처리하는 방식
    • <input type="file">을 이용하고 Ajax로 처리
    • HTML5의 Drag And Drop 기능이나 jQuery 라이브러리를 이용해서 처리하는 방식
    • Ajax 통신을 이용한 파일업로드 예제 코드
      • 업로드 버튼이 눌럿을 때 개발자 도구 - 콘솔에 오류가 표시되는게 정상적임
      • 해당 에러가 발생했을 때 업로드 폴더 확인 필수!
  • 파일 업로드시 고려해야되는 점
    • 동일한 이름으로 파일이 업로드 되었을 때 기존 파일이 사라지는 문제
    • 이미지 파일의 경우 원본 파일의 용량이 큰 경우 섬네일 이미지를 생성해야하는 문제
    • 이미지 파일과 일반 파일을 구분해서 다운로드 혹은 페이지에서 조회하도록 처리하는 문제
    • 첨부파일 공격에 대비하기 위한 업로드 파일의 확장자 제한

파일의 확장자나 크기의 사전 처리

  • 첨부파일을 이용한 웹 공격을 막기 위해 행해지는 조치
  • 확장자 제한은 JavaScript에서 정규표현식을 이용
    var regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$");
    var maxSize = 5242880;
    	
    function checkExtension(fileName, fileSize){
    	if(fileSize >= maxSize){
    		alert("파일 사이즈 초과");
    		return false;
    	}
    	
    	...
    
    	if(regex.test(fileName)){
    		alert("해당 종류의 파일은 업로드할 수 없습니다.");
    		return false;
    	}
    	return true;
    }
    
    ... 이후 해당 함수를 이용해 체크

중복된 이름의 첨부파일 처리

  • 중복된 이름 파일 처리
    • 현재 시간을 밀리세컨드까지 구분해서 파일 이름을 생성해서 저장
    • UUID를 이용해서 중복이 발생할 가능성이 거의 없는 문자열로 생성
      • java.util.UUID의 값을 이용
        @PostMapping("/uploadAjaxAction")
        public void uploadAjaxPost(MultipartFile[] uploadFile) {
        	...
        
        		// 파일 이름 설정
        	String uploadFileName = multipartFile.getOriginalFilename();
        	uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\")+1);
        	log.info("only file name : " + uploadFileName);
        	UUID uuid = UUID.randomUUID();
        	uploadFileName = uuid.toString() + "_" + uploadFileName;
        	
        	File saveFile = new File(uploadPath, uploadFileName);
        	
        	...
        }
  • 한 폴더 내에 생성할 파일 개수 문제
    • 한 폴더에 너무 많은 파일이 있는 경우 속도 저하, 개수의 제한 문제를 해결하기 위해 일반적으로 '년/월/일' 단위의 폴더를 생성
      private String getFolder() {
      	SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
      	Date date = new Date();
      	String str = sdf.format(date);
      	return str.replace("-", File.separator);
      }
      
      @PostMapping("/uploadAjaxAction")
      public void uploadAjaxPost(MultipartFile[] uploadFile) {
      	...
      	
      	// '년/월/일' 폴더 생성
      	File uploadPath = new File(uploadFolder,getFolder());
      	log.info("upload path : " + uploadPath);
      	
      	if(uploadPath.exists() == false) {
      		uploadPath.mkdirs();
      	}
      
      	...
      }

섬네일 이미지 생성

  • 섬네일 생성 단계
    • 업로드된 파일이 이미지 종류의 파일인지 확인
    • 이미지 파일의 경우에는 섬네일 이미지 생성 및 저장
  • 일반 파일과 이미지 파일 구분
    • Thumbnailator 라이브러리를 이용
      <dependency>
      	<groupId>net.coobird</groupId>
      	<artifactId>thumbnailator</artifactId>
      	<version>0.4.8</version>
      </dependency>
    • 컨트롤러
      // 
      private boolean checkImageType(File file) {
      	try {
      		String contentType = Files.probeContentType(file.toPath());
      		
      		return contentType.startsWith("image");
      	} catch (IOException e) {
      		e.printStackTrace();
      	}
      	return false;
      }
      @PostMapping("/uploadAjaxAction")
      public void uploadAjaxPost(MultipartFile[] uploadFile) {
      	...
      	try {
      		// 파일 저장
      		File saveFile = new File(uploadPath, uploadFileName);
      		multipartFile.transferTo(saveFile);
      		
      		if(checkImageType(saveFile)) {
      			FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath, "s_" + uploadFileName));
      			
      			Thumbnailator.createThumbnail(multipartFile.getInputStream(), thumbnail, 100, 100);
      				
      			thumbnail.close();
      		}
      	} catch (Exception e) {
      		e.printStackTrace();
      	}
      }

업로드된 파일의 데이터 변환

  • 브라우저 쪽으로 전달해야하는 데이터
    • 업로드된 파일의 이름과 원본 파일의 이름
    • 파일이 저장된 경로
    • 업로드된 파일이 이미지인지 아닌지에 대한 정보
  • 객체를 생성해 처리하는 방법
    • pom.xml
      <!-- jackson-databind -->
      <dependency>
      	<groupId>com.fasterxml.jackson.core</groupId>
      	<artifactId>jackson-databind</artifactId>
      	<version>2.9.5</version>
      </dependency>
      
      <!-- jackson-dataformat-xml -->
      <dependency>
      	<groupId>com.fasterxml.jackson.dataformat</groupId>
      	<artifactId>jackson-dataformat-xml</artifactId>
      	<version>2.9.5</version>
      </dependency>
    • 컨트롤러

브라우저에서 섬네일 처리

  • 업로드 후 업로드 부분을 초기화 시키는 작업
    • <input type="file"> 태그 초기화
      // input태그 복사
      var cloneObj = $(".uploadDiv").clone();
      
      $("#uploadBtn").on("click", function(e){
      	...
      
      	$.ajax({
      		url:'/controller/uploadAjaxAction',
      		processData: false,
      		contentType: false,
      		data: formData,
      		type:'post',
      		dataType:'json',
      		success: function(result){
      			console.log(result);
      			
      			// input태그 위치에 다시 추가
      			$(".uploadDiv").html(cloneObj.html());
      		}
      	});
      });
  • 결과 데이터를 이용해 화면에 섬네일이나 파일 이미지를 보여주는 작업
    • 화면에 선택한 파일 표시

      ...
      
      <style type="text/css">
      .uploadResult{
      	width : 100%;
      	background-color : gray;
      }
      
      .uploadResult ul{
      	display: flex;
      	flex-flow: row;
      	justify-content: center;
      	align-items: center;
      }
      
      .uploadResult ul li {
      	list-style: none;
      	padding: 10px;
      }
      
      .uploadResult ul li img{
      	width: 20px;
      }
      </style>
      
      ...
      
      <script type="text/javascript">
      
      ...
      
      var uploadResult = $(".uploadResult ul");
      		
      function showUploadedFile(uploadResultArr){
      	var str = "";
      			
      	$(uploadResultArr).each(function(i, obj){
      		if(!obj.image){
      			// 이미지일 때
      			str += "<li><img src='/resources/img/attach.png'" + obj.fileName + "</li>";
      		}else{
      			// 일반 파일 일때
      			str += "<li>" + obj.fileName + "</li>";
      		}
      	});
      			
      	uploadResult.append(str);
      }
      
      ...
      
      </script>
    • 컨트롤러에서 섬네일 데이터 전송

      // Controller
      // 문자열로 파일의 경로가 포함된 fileName을 파라미터로 받고 byte 배열을 전송
      @GetMapping("/display")
      @ResponseBody
      public ResponseEntity<byte[]> getFile(String fileName){
      	log.info("fileName : " + fileName);
      	
      	File file = new File("c:\\upload\\"+fileName);
      	
      	log.info("file : " + file);
      	
      	ResponseEntity<byte[]> result = null;
      	
      	try {
      		HttpHeaders header = new HttpHeaders();
      		
      		// byte 배열로 이미지 파일을 전송할 때 브라우저에 보내주는 MIME 타입이 파일의 종류에 따라 달라짐.
      		// probeContentType 메소드를 이용해 적절한 MIME타입 데이터를 Http 헤더 메시지에 포함할 수 있도록 처리
      		header.add("Content-Type", Files.probeContentType(file.toPath()));
      		result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
      	} catch (IOException e) {
      		e.printStackTrace();
      	}
      	return result;
      }
      function showUploadedFile(uploadResultArr){
      	var str = "";
      	
      	$(uploadResultArr).each(function(i, obj){
      		if(!obj.image){
      			// 이미지일 때
      			str += "<li><img src='/resources/img/attach.png'" + obj.fileName + "</li>";
      		}else{
      			// 일반 파일 일때
      			// str += "<li>" + obj.fileName + "</li>";
      			
      			// 파일 경로 인코딩
      			var fileCallPath = encodeURIComponent(obj.uploadPath + "/s_" + obj.uuid + "_" + obj.fileName);
      			
      			str += "<li><img src='/controller/display?fileName="+fileCallPath+"'></li>";
      		}
      	});
      			
      	uploadResult.append(str);
      }

첨부파일 다운로드 / 원본 이미지 보여주기

  • 첨부파일 다운로드

    • 컨트롤러

      // 파일을 다운 받기 위해 MIME 타입을 application/octet-stream으로 설정
      // 사용자가 사용중인 브라우저를 @RequestHeader를 통해 HTTP 헤더 메시지의 User-Agent 데이터를 가져옴
      @GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
      @ResponseBody
      public ResponseEntity<Resource> downloadFile(@RequestHeader("User-Agent") String userAgent, String fileName){
      
      	// 업로드한 파일 경로 가져오기
      	Resource resource = new FileSystemResource("C:\\upload\\" + fileName);
      
      	// 업로드한 파일이 있는지 없는지 체크
      	if(resource.exists() == false) {
      		return new ResponseEntity<>(HttpStatus.NOT_FOUND);// 없으면 not_found 상태 리턴
      	}
      
      	// 파일이름 가져오기
      	String resourceName = resource.getFilename();
      	
      	// UUID 제거
      	String resourceOriginalName = resourceName.substring(resourceName.indexOf("_")+1); 
      
      	HttpHeaders headers = new HttpHeaders();
      	
      	try {
      		String downloadName = null;
      		
      		// 브라우저별 인코딩 처리
      		if(userAgent.contains("Trident")) {
      			log.info("IE browser");
      			
      			downloadName = URLEncoder.encode(resourceOriginalName,"UTF-8").replaceAll("\\", " ");
      		}
      		else if(userAgent.contains("Edge browser")) {
      			log.info("Edge browser");
      				
      			downloadName = URLEncoder.encode(resourceOriginalName,"UTF-8");
      		}
      		else {
      			log.info("Chrome browser");
      
      			downloadName = new String(resourceOriginalName.getBytes("UTF-8"),"ISO-8859-1");
      		}
      		
      		log.info("downloadName" + downloadName);
      		
      		// 헤더 설정
      		// Content-Disposition을 통해 파일 이름 문자열 처리할 때 한글 깨지는 문제 막기 위해 사용
      		// filename에 다운로드할 파일 설정
      		headers.add("Content-Disposition", "attachment; filename=" + downloadName);
      	} catch (UnsupportedEncodingException e) {
      	e.printStackTrace();
      	}
      
      	return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
      }
    • View 처리

      <script type="text/javascript">
      	function showUploadedFile(uploadResultArr){
      		var str = "";
      			
      		$(uploadResultArr).each(function(i, obj){
      			if(!obj.image){
      				var fileCallPath = encodeURIComponent(obj.uploadPath + "/" + obj.uuid + "_" + obj.fileName);
      				
      				// a태그를 이용해 download 컨트롤러로 접근하여
      				// 클릭했을 때 다운로드 진행
      				str += "<li><div><a href='/controller/download?fileName="+fileCallPath+"'>"
      						+ "<img src='/controller/resources/img/attach.png'>"+obj.fileName+"</a>"+
      						"<span data-file=\'"+fileCallPath+"\' data-type='file'>x</span></div></li>";
      			}else{
      				...
      			}
      		});
      	
      		uploadResult.append(str);
      	}
      </script>
  • 원본 이미지 보여주기

    • View 처리
      // 원본 파일 보여주기
      // $(document).ready() 바깥에 작성한 이유는 a태그에서 직접 호출할 수 있는 방식으로 작성하기 위해
      function showImage(fileCallPath){
      	$(".bigPictureWrapper").css("display","flex").show();
      	
      	$(".bigPicture")
      	.html("<img src='/controller/display?fileName="+encodeURI(fileCallPath)+"'>")
      	.animate({width:'100%', height:'100%'},1000);
      }
      
      $(document).ready(function(){
      	function showUploadedFile(uploadResultArr){
      		...
      
      		$(uploadResultArr).each(function(i, obj){
      			if(!obj.image){
      				// 기존에 "/s_"로 파일 경로를 잡아준 부분을 "/"로 변경
      				var fileCallPath = encodeURIComponent(obj.uploadPath + "/" + obj.uuid + "_" + obj.fileName);
      				...
      			}else{
      				...
      			}
      		});
      		...
      	}
      	
      	...
      	
      	// 원본 이미지 숨기기
      	$(".bigPictureWrapper").on("click",function(e){
      		$(".bigPicture").animate({width:'0%',height:'0%'},1000);
      		setTimeout(()=>{
      			$(".bigPictureWrapper").hide();
      		},1000)
      	});
      });
  • 첨부파일 삭제

    • 컨트롤러
      @PostMapping("/deleteFile")
      @ResponseBody
      public ResponseEntity<String> deleteFile(String fileName, String type){
      	
      	File file;
      	
      	try {
      		// 파일 경로 설정
      		file = new File("C:\\upload\\" + URLDecoder.decode(fileName,"UTF-8"));
      		
      		file.delete();
      		
      		// 이미지일 때
      		if(type.equals("image")) {
      			String largeFileName = file.getAbsolutePath().replace("s_","");
      			log.info("largeFileName" + largeFileName);
      			file = new File(largeFileName);
      			file.delete();
      		}
      	} catch (UnsupportedEncodingException e) {
      		// TODO Auto-generated catch block
      		e.printStackTrace();
      		return new ResponseEntity<>(HttpStatus.NOT_FOUND);
      	}
      	return new ResponseEntity<>("deleted", HttpStatus.OK);
      }
    • View 처리
      // 첨부파일 삭제
      	$(".uploadResult").on("click","span", function(e){
      		var targetFile = $(this).data("file");
      		var type = $(this).data("type");
      		
      		console.log(targetFile);
      			
      		$.ajax({
      			url:'/controller/deleteFile',
      			data:{fileName:targetFile, type:type},
      			dataType:'text',
      			type:'POST',
      			success:function(result){
      				alert(result);
      			}
      		});
      	});

✏️ Spring Security

Spring Security란?

  • Spring 기반의 애플리케이션의 보안을 담당하는 스프링 하위 프레임워크
  • 인증과 권한에 대한 부분을 Filter 흐름에 따라 처리
  • 보안 관련 옵션들을 제공해주어 개발자가 보안 로직을 작성하지 않아도 되는 장점을 가짐

Filter와 Interceptor

  • Filter
    • 디스패처 서블릿에 요청이 전달되기 전/후에 URL 패턴에 맞는 모든 요청에 대해 부가작업을 처리할 수 있는 기능을 제공
    • 웹 컨테이너에 의해 관리되며, 디스패처 서블릿 전/후에 처리
    • Filter 메소드
      public interface Filter{
      	// 필터 객체 초기화 및 서비스 추가하기 위한 메소드
      	// 웹 컨테이너가 처음에 init 메소드를 호출, 이후부터는 doFilter를 통해 처리
      	public default void init(FilterConfig filterConfig) throws ServletException;
      
      	// url-pattern에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기전 웹 컨테이너에 의해 실행되는 메소드
      	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
      			throws IOException, ServletException;
      
      	// 필터 객체를 서비스에서 제거하고 사용하는 자원을 반환하기 위한 메소드
      	public default void destroy();
      }
    • Filter 용도
      • 공통된 보안 및 인증/인가 관련 작업, 모든 요청에 대한 로깅/감사 등 스프링과 무관하게 전역적으로 처리해야하는 작업들
      • 웹 애플리케이션에 전반적으로 사용되는 기능 구현
  • interceptor
    • 디스패처 서블릿이 컨트롤러를 호출하기 전/후 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공
    • 스프링 컨텍스트 내부에서 동작
    • 디스패처 서블릿이 핸들링 매핑을 통해 컨트롤러 요청의 결과로 실행 체인을 반환하는데, 이 실행 체인에 인터센터가 등록되어있으면 순차적으로 인터센터를 실행한 후 컨트롤러가 실행
    • interceptor 메소드
      public interface HandlerInterceptor{
      	// 컨트롤러가 호출되기 전에 실행
      	// 컨트롤러 이전에 처리해야하는 전처리 작업이나 요청 정보를 가공하거나 추가하는 경우 사용
      	// handler 파리미터는 핸들링 매핑이 찾아준 컨트롤러 빈에 매핑되는 HandlerMethod라는 객체, 
      	// 	@RequestMapping이 붙은 메소드의 정보를 추상화한 객체 
      	default boolean preHandle(ServletRequest request, ServletResponse response, Object handler) throws Exception;
      
      	// 컨트롤러를 호출된 후에 실행
      	// 컨트롤러 이후 처리해야하는 후처리 작업이 있을 때 사용
      	// 하위 컨트롤러에서 작업을 진행하다 예외가 발생하면 실행안됨
      	default void postHandle(ServletRequest request, ServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception;
      
      	// 모든 뷰에서 최종 결과를 생성하는 일을 포함해 모든 작업이 완료된 후에 실행
      	// 요청 처리 중에 사용한 리소스를 반환할 때 사용하기 적합
      	// 하위 컨트롤러에서 작업을 진행하다 예외가 발생하더라고 실행됨
      	default void afterCompletion((ServletRequest request, ServletResponse response, Object handler, @Nullable Exception ex) throws Exception;
      }
    • interceptor 용도
      • 클라이언트의 요청과 관련되어 전역적으로 처리해야하는 작업
      • 컨트롤러로 넘겨주기 위한 정보를 가공할 때

Spring Web Security 설정

  • pom.xml
    • 스프링 시큐리티 관련 라이브러리 설정
      <dependency>
      	<groupId>org.springframework.security</groupId>
      	<artifactId>spring-security-web</artifactId>
      	<version>Spring Security 버전</version>
      </dependency>
      <dependency>
      		<groupId>org.springframework.security</groupId>
      	<artifactId>spring-security-config</artifactId>
      	<version>Spring Security 버전</version>
      </dependency>
      <dependency>
      	<groupId>org.springframework.security</groupId>
      	<artifactId>spring-security-core</artifactId>
      	<version>Spring Security 버전</version>
      </dependency>
      <dependency>
      	<groupId>org.springframework.security</groupId>
      	<artifactId>spring-security-taglibs</artifactId>
      	<version>Spring Security 버전</version>
      </dependency>
  • security-context.xml
    • root-context.xml이 있는 위치에 'New' - 'Spring Bean Configuration File'을 통해 security-context.xml 생성
    • xml 파일 하단부분에 Namespaces를 통해 security항목 추가
    • 스프링 시큐리티 5버전 주의사항
  • web.xml
    • 스프링 시큐리티를 스프링 MVC 동작에 관여하도록 설정
      ...
      
      <!-- 스프링 시큐리티가 스프링 MVC에서 사용되기 위해 필터를 이용해 스프링 동작에 관여하도록 설정 -->
      <filter>
      	<filter-name>springSecurityFilterChain</filter-name>
      	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
      </filter>
      
      <filter-mapping>
      	<filter-name>springSecurityFilterChain</filter-name>
      	<url-pattern>/*</url-pattern>
      </filter-mapping>
      
      <!-- 
      	위 필터만 작성하면 NoSuchBeanDefinitionException이 발생하는데
      	이 예외는 springSecurityFilterChain라는 빈이 설정되지 않아서 발생하는 문제
      	-> 스프링 시큐리티 설정파일을 찾을 수 없어서 발생한 문제로 생각하면됨.
      	
      	context-param을 통해 security-context.xml 파일 로딩과 해당 파일에 최소한의 설정이 필요
      -->
      <context-param>
      	<param-name>contextConfigLocation</param-name>
      	<param-value>/WEB-INF/spring/root-context.xml
      	/WEB-INF/spring/security-context.xml
      	</param-value>
      </context-param>
      
      ...

인증(Authentication)과 권한 부여/인가(Authorization)

  • 인증(Authentication)
    • 해당 사용자가 본이 맞는지 확인하는 절차
  • 권한 부여/인가(Authorization)
    • 인증된 사용자가 요청한 자원에 접근 가능한지를 결정하는 절차
  • 스프링 시큐리티는 인증 절차를 거친 후 권한 부여 절차를 진행
  • AuthenticationManager
    • 다양한 방식의 인증을 처리할 수 있도록 하위의 AuthenticationProvider를 찾아 실제 인증 역할을 위임
  • ProviderManager
    • AuthenticationManager 인터페이스를 상속받아 구현한 구현체
    • AuthenticationProvider에게 인증 역할을 위임
    • 모든 provider에 대한 대비책 역할
    • ProviderManager를 사용함에 따라 프로그램 내에서 서로 다른 인증 메커니즘을 지원 가능
  • AuthenticationProvider
    • 실제로 인증 역할

로그인과 로그아웃 처리

  • security-context.xml

    <!-- 직접 작성한 Handler 클래스를 이용하기 위해 Bean에 저장 -->
    <bean id="customAccessDenied" class="org.zerock.security.CustomAccessDeniedHandler"></bean>
    <bean id="customLoginSuccess" class="org.zerock.security.CustomLoginSuccessHandler"></bean>
    	
    <security:http>
    	<!-- 
    		security:intercept-url
    		- pattern : URI 패턴
    		- access : 권한 체크
    		 -->
    	<security:intercept-url pattern="/sample/all" access="permitAll"/>
    	<security:intercept-url pattern="/sample/member" access="hasRole('ROLE_MEMBER')"/>
    	<security:intercept-url pattern="/sample/admin" access="hasRole('ROLE_ADMIN')"/>
    	
    	<!-- 에러 페이지 -->
    	<security:access-denied-handler ref="customAccessDenied"/>
    
    	<!-- 
    		로그인 페이지
    		- 로그인을 성공했을 때 authentication-success-handler-ref에 설정된 핸들러로 진입
    	-->
    	<security:form-login login-page="/customLogin" authentication-success-handler-ref="customLoginSuccess"/>
    	
    	<!-- 
    		로그아웃 페이지 
    		- 로그아웃 시 logout-url에 설정된 페이지로 요청
    		- invalidate-session을 통해 세션정보를 제거
    	-->
    	<security:logout logout-url="/customLogout" invalidate-session="true"/>
    		
    </security:http>
    	
    <security:authentication-manager>
    	<security:authentication-provider>
    		<security:user-service>
    			<!-- 계정 설정 및 권한 부여 -->
    			<security:user name="member" password="{noop}member" authorities="ROLE_MEMBER"/>	
    			<security:user name="admin" password="{noop}admin" authorities="ROLE_MEMBER, ROLE_ADMIN"/>
    		</security:user-service>
    	</security:authentication-provider>
    </security:authentication-manager>
  • 컨트롤러

    // 에러 페이지
    @GetMapping("/accessError")
    public void accessDenied(Authentication auth, Model model) {
    	log.info("access Denied : " + auth);
    	
    	model.addAttribute("msg", "Access Denied");
    }
    	
    // 로그인 페이지
    @GetMapping("/customLogin")
    public void loginInput(String error, String logout, Model model) {
    	log.info("error : " + error);
    	log.info("logout : " + logout);
    	
    	if(error != null) {
    		model.addAttribute("error", "Login Error Check Your Account");
    	}
    	
    	if(logout != null) {
    		model.addAttribute("logout", "Logout!!");
    	}
    }
    
    // 로그아웃 페이지
    @GetMapping("/customLogout")
    public void logoutGet() {
    	log.info("custom logout");
    }
  • 핸들러

    public class CustomAccessDeniedHandler implements AccessDeniedHandler{
    
    	// 에러 페이지 핸들러
    	// 로그인 실패시 에러 페이지로 리다이렉트
    	@Override
    	public void handle(HttpServletRequest request, HttpServletResponse response,
    			AccessDeniedException accessDeniedException) throws IOException, ServletException {
    		log.error("Access Denied Handler");
    		
    		log.error("Redirect.............");
    		
    		response.sendRedirect("/controller/accessError");
    	}
    }
    public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler{
    	
    	// 로그인 성공 페이지
    	// security-context.xml에서 계정 설정 때 부여한 권한을 가져와 roleNames 리스트에 추가
    	// if문을 통해 ROLE NAME에 따른 처리
    	@Override
    	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
    			Authentication auth) throws IOException, ServletException {
    	
    		log.warn("Login Success");
    		
    		List<String> roleNames = new ArrayList<>();
    		
    		auth.getAuthorities().forEach(authority -> {
    			roleNames.add(authority.getAuthority());
    		});
    		
    		log.warn("ROLE NAMES : " + roleNames);
    	
    		if(roleNames.contains("ROLE_ADMIN")) {
    			response.sendRedirect("/controller/sample/admin");
    			return;
    		}
    		
    		if(roleNames.contains("ROLE_MEMBER")) {
    			response.sendRedirect("/controller/sample/member");
    			return;
    		}
    		
    		response.sendRedirect("/");
    	}
    
    }
  • CSRF(Cross-site request forgery) 공격과 토큰

    • 사이트 사이의 위조 요청으로 사용자가 자신의 의지와 무관하게 공격자가 의도한 행위를 웹사이트에 요청하기 하는 공격
  • CSRF 공격 방지 방법

    • Referrer 검증
      • request header에 있는 요청을 한페이지의 정보가 담긴 referrer 속성을 검증하여 차단
    • CSRF Token 사용
      • 랜덤값을 사용자의 세션에 저장하고 모든 요청을 다 이 토큰을 이용해 검증
    • CAPTCHA 사용
      • 로봇인지 아닌지 체크하는 라이브러리를 사용해 사용자가 의도한 요청인지 트리거로 동작하는 요청인지 검증
  • 스프링 시큐리티에서 CSRF 설정

    <security:csrf disabled="false"/>

JDBC를 이용하는 간편 인증/권한 처리

  • 지정된 형식으로 테이블을 생성해서 사용하는 방식
    • JdbcUserDetailsManager 클래스와 같이 SQL을 이용하는 공개 코드에 맞게 테이블을 생성
    • security-context.xml 수정
      ...
      
      <!-- 커스텀 패스워드 인코더 설정 -->
      <bean id="customPasswordEncoder" class="org.zerock.security.CustomNoOpPasswordEncoder"></bean>
      
      ...
      
      <security:authentication-manager>
      	<security:authentication-provider>
      		<!-- 계정 설정 및 권한 부여 부분
      				이전 코드
      				<security:user-service>
      					<security:user name="member" password="{noop}member" authorities="ROLE_MEMBER"/>	
      					<security:user name="admin" password="{noop}admin" authorities="ROLE_MEMBER, ROLE_ADMIN"/>
      				</security:user-service>
      			
      			JDBC 연결
      			data-source-ref : JDBC 연결 정보
      		-->
      		<security:jdbc-user-service data-source-ref="dataSource" />
      
      		<security:password-encoder ref="customPasswordEncoder"/>
      	</security:authentication-provider>
      </security:authentication-manager>
    • 패스워드 인코더
      // 패스워드 인코딩 관련
      // 패스워드는 무조건 단방향으로 암호화
      // PasswordEncoder 구현 클래스에 다양한 암호화 메소드를 지원
      // - BcryptPasswordEncoder
      // - Argon2PasswordEncoder
      // - Pbkdf2PasswordEncoder
      // - ScryptPasswordEncoder
      
      // PasswordEncoder 구현 클래스에서 지원하는 메소드를 사용하면 해당 패스워드 인코더 핸들러를 안만들어도됨.
      @Log4j2
      public class CustomNoOpPasswordEncoder implements PasswordEncoder{
      	// 암호화할 때 사용
      	@Override
      	public String encode(CharSequence rawPassword) { 
      		log.warn("before encode : " + rawPassword);
      		return rawPassword.toString();
      	}
      
      	// 사용자에게서 입력받은 패스워드를 비교
      	@Override
      	public boolean matches(CharSequence rawPassword, String encodedPassword) { 
      		log.warn("matches : " + rawPassword + " : " + encodedPassword);
      		return rawPassword.toString().equals(encodedPassword);
      	}
      }
  • 기존에 작성된 데이터 베이스를 이용하는 방식
      • security-context.xml 수정
        ...
        
        <!-- 스프링 시큐리티에서 지원하는 BcryptPasswordEncoder 사용 -->
        <bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
        
        ...
        
        <security:authentication-manager>
        	<security:authentication-provider>
        		<!-- 계정 설정 및 권한 부여 부분
        				이전 코드
        				<security:user-service>
        					<security:user name="member" password="{noop}member" authorities="ROLE_MEMBER"/>	
        					<security:user name="admin" password="{noop}admin" authorities="ROLE_MEMBER, ROLE_ADMIN"/>
        				</security:user-service>
        			
        			JDBC 연결
        			data-source-ref : JDBC 연결 정보
        			users-by-username-query속성과 authorities-by-username-query속성을 이용하면 
        			기존 테이블을 이용해 사용자 인증 및 권한 확인할 수 있음.
        		-->
        		<security:jdbc-user-service 
        			data-source-ref="dataSource" 
        			users-by-username-query="select userid, userpw, enabled from tb1_member where userid = ? "
        			authorities-by-username-query="select userid,auth from tb1_member_auth where userid = ? "
        		/>
        
        		<security:password-encoder ref="bcryptPasswordEncoder"/>
        	</security:authentication-provider>
        </security:authentication-manager>

커스텀 UserDetailsService

  • JDBC를 이용해 사용자 상세 정보를 처리하기엔 충분하지 못함
  • 위 문제를 해결하기 위해 UserDetailsService를 구현
    • 개발자가 원하는 객체를 인증과 권한 체크에 활용할 수 있음
  • UserDetailsService 인터페이스
    • loadUserByUsername() 메소드
      • 해당 메소드는 사용자의 정보와 권한 정보 등을 담을 수 있는 UserDetails 타입을 반환
  • UserDetails 타입
    • 사용자의 정보와 권한 정보 등을 담는 타입
    • getAuthorities(), getPassword(), getUserName() 등 추상메소드를 가짐
  • 사용자 정보 및 권한 VO
    • 사용자 정보
      // 여러 개의 사용자 권한을 가질 수 있게 설계
      // 사용자 정보 VO
      @Data
      public class MemberVO {
      	private String userid;
      	private String userpw;
      	private String userName;
      	private String enabled;
      	
      	private Date regDate;
      	private Date updateDate;
      	private List<AuthVO> authList;
      }
    • 사용자 권한
      @Data
      public class AuthVO {
      	private String userid;
      	private String auth;
      }
  • Mapper와 MyBatis 처리
    • MemberMapper 인터페이스
      public interface MemberMapper {
      	public MemberVO read(String userid);
      }
    • MyBatis 처리
      • 사용자 정보 테이블과 사용자 권한 테이블을 조인을 통해 한번에 처리하기 위해 MyBatis의 ResultMap을 이용해서 처리
        <mapper namespace="org.zerock.mapper.MemberMapper">
        	<resultMap type="org.zerock.domain.MemberVO" id="memberMap">
        		<id property="userid" column="userid"/>
        		<result property="userid" column="userid"/>
        		<result property="userpw" column="userpw"/>
        		<result property="userName" column="username"/>
        		<result property="regDate" column="regdate"/>
        		<result property="updateDate" column="updatedate"/>
        		<collection property="authList" resultMap="authMap"></collection>
        	</resultMap>
        	
        	<resultMap type="org.zerock.domain.AuthVO" id="authMap">
        		<result property="userid" column="userid"/>
        		<result property="auth" column="auth"/>
        	</resultMap>
        	
        	<select id="read" resultMap="memberMap">
        	 	SELECT
        			mem.userid, userpw, username, enabled, regdate, updatedate, auth
        		FROM
        			tb1_member mem LEFT OUTER JOIN tb1_member_auth auth on mem.userid = auth.userid
        		WHERE mem.userid = #{userid}
        	</select>
        </mapper>
    • UserDetailsService 구현
      @Log4j2
      public class CustomUserDetailsService implements UserDetailsService{
      	@Setter(onMethod_ = @Autowired)
      	private MemberMapper memberMapper;
      
      	@Override
      	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      	
      		log.warn("Load User By UserName : " + username);
      		
      		// username을 통해 해당 유저 정보 가져오기
      		MemberVO vo = memberMapper.read(username);
      	
      		log.warn("queried by member mapper : " + vo);
      	
      		return vo == null ? null : new CustomUser(vo);
      	}
      }
    • security-context.xml
      <security:authentication-manager>
      	<!-- userDetailsService 클래스를 빈으로 생성하여 user-service-ref에 할당 -->
      	<security:authentication-provider user-service-ref="customUserDetailsService">
      		<security:password-encoder ref="bcryptPasswordEncoder"/>
      	</security:authentication-provider>
      </security:authentication-manager>
  • MemberVO를 UsersDetails 타입으로 변환
    • MemberVO 클래스를 수정해되 되지만 UserDetails 인터페이스를 구현하여 기존 클래스를 수정하지 않고 확장
      // 스프링 시큐리티의 User 클래스를 상속
      // 
      @Getter
      public class CustomUser extends User{
      	private static final long serialVersionUID = 1L;
      	
      	private MemberVO member;
      	
      	public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
      		super(username, password, authorities);
      	}
      	
      	public CustomUser(MemberVO vo) {
      		// AuthVO 인스턴스를 GrantedAuthority 객체로 변환
      		// stream과 map을 이용해서 처리
      		super(vo.getUserid(), vo.getUserpw(), vo.getAuthList().stream().map(auth -> new SimpleGrantedAuthority(auth.getAuth())).collect(Collectors.toList()));
      	}
      }

스프링 시큐리티를 JSP에서 사용하기

  • UserDetailsService와 같이 별도의 인증/권한체크를 하는 가장 큰 이유는 JSP등에서 단순히 사용자의 아이디 정보가 아닌 사용자의 이름, 이메일과 같은 추가적인 정보를 이용하기 위해 사용
  • JSP
    ...
    
    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
    <%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
    ...
    
    <body>
    	<h1>/sample/admin page</h1>
    	<!-- 
    		<sec:authentication property="principal"/> : UserDetailsService에서 반환된 객체
    													 CustomUserDetailsService를 이용했으면 CustomUser를 반환
    	 -->
    	<p>principal : <sec:authentication property="principal"/></p>
    	<p>MemberVO : <sec:authentication property="principal.member"/></p>
    	<p>사용자이름 : <sec:authentication property="principal.member.userName"/></p>
    	<p>사용자아이디 : <sec:authentication property="principal.username"/></p>
    	<p>사용자 권한 리스트 : <sec:authentication property="principal.member.authList"/></p> 
    
    	<a href="/controller/customLogout">Logout</a>
    </body>
    </html>
  • 표현식을 이용하는 동적 화면 구성
    • hasRole(role) / hasAuthority(auth) : 해당권한이 있으면 true
    • hasAnyRole(role1, role2, ... ) / hasAnyAuthority(auth) : 여러 권한 중 하나라도 해당하는 권한이 있으면 true
    • principal : 현재 사용자 정보를 의미
    • permitAll : 모든 사용자에게 허용
    • denyAll : 모든 사용자에게 거부
    • isAnonymous() : 익명 사용자인 경우
    • isAuthenticated() : 안증된 사용자면 true
    • isFullyAuthenticated() : Remember-me로 인증된 것이 아닌 인증된 사용자인 경우 true
    • 사용 예제
      // 익명 사용자인경우
      <sec:authorize access="isAnonymous()">
      	<a href="/controller/customLogin">로그인</a>
      </sec:authorize>
      
      // 인증된 사용자인 경우
      <sec:authorize access="isAuthenticated()">
      	<a href="/controller/customLogout">로그아웃</a>
      </sec:authorize>

자동 로그인(remember-me)

  • 자동 로그인 기능은 대부분 쿠키를 이용해 구현
  • 스프링 시큐리티에서는 <security:remember-me> 태그를 이용해 기능 구현
  • <security:remember-me> 태그 속성
    • key : 쿠키에 사용되는 값을 암호화하기 위한 키 값
    • data-source-ref : 브라우저에 보관되는 쿠키의 이름 지정
    • remember-me-cookie : 브라우저에 보관되는 쿠키의 이름 지정, 기본값은 remember-me
    • remember-me-parameter : 웹 화면에서 로그인할 때 'remember-me'는 대부분 체크박스 이용해서 처리, 체크박스 태그는 name 속성을 의미
    • token-validity-seconds : 쿠키의 유효시간을 지정
  • security-context.xml
    <security:http>
    	...
    
    		<!-- 
    			로그아웃 페이지 
    			- 로그아웃 시 logout-url에 설정된 페이지로 요청
    		- invalidate-session을 통해 세션정보를 제거
    		- delete-cookies를 통해 자동로그인 쿠키 삭제
    	-->
    	<security:logout logout-url="/customLogout" invalidate-session="true" delete-cookies="remember-me, JSESSION_ID"/>
    		
    	<!-- 
    		자동 로그인
    		- key : 쿠키에 사용되는 값을 암호화하기 위한 키 값
    		- data-source-ref : DataSource를 지정하고 테이블을 이용해서 기존 로그인 정보를 기록(옵션)
    		- remember-me-cookie : 브라우저에 보관되는 쿠키의 이름을 지정, 기본값은 remember-me
    		- remember-me-parameter : 웹 화면에서 로그인할 때 'remember-me'는 대부분 체크박스를 이용해 처리, 체크박스 태그는 name 속성을 의미
    		- token-validity-seconds : 쿠키 유효시간 지정
    	-->
    	<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800"/>
    </security:http>

어노테이션을 이용하는 스프링 시큐리티 설정

  • @Secured(role) : 스프링 시큐리티 초기부터 사용, () 안에 role을 입력
  • @preAuthorize(..), @PostAuthorize(..) : 3버전부터 지원, () 안에 표현식을 사용
  • 컨트롤러에 해당 어노테이션을 설정하여 스프링 시큐리티 설정
  • servlet-context.xml 설정
    ...
    
    // pre-post-annotations와 secured-annotations 등이 enabled로 설정되어 있어야
    // 해당 어노테이션을 사용할 수 있음
    <security:global-method-security pre-post-annotations="enabled" secured-annotations="enabled"></security:global-method-security>
    
    ... 

참고한 책

  • 코드로 배우는 스프링 웹 프로젝트 개정판

참고 사이트

profile
프로그래밍 언어 공부 정리

0개의 댓글