반응형

서버 특정 경로에 이미지파일이 존재할때 해당 파일의 경로나 파일등의 정보를 노출하지 않고 바이너리 데이터를 통해 이미지를 표현하고 싶었습니다.

특히나 개인정보가 담긴 이미지 등의 데이터는 더욱 민감할 수 있는데 이런 형태의 인증 처리가 없을 경우 최악으로 인가되지 않은 특정 클라이언트가 url/경로/파일명 형태로 마구잡이로 접근하여 개인정보를 가져갈 수 있는 위험이 있다.

 

해당 위치 접근을 바로 할 수 없도록 막았고 가져오기 위해 인증된 계정만 서버에서 데이터를 전달하도록 처리하였다.

 

Controller.java

@Controller
public class AdminController {
	
	private final Logger logger = Logger.getLogger(AdminController.class);
    
	@RequestMapping(value={"preView"}, method=RequestMethod.POST)
	private @ResponseBody Map<String, Object> preView(@RequestParam Map<String, Object> params, final HttpServletRequest request) throws Exception {
		String fileName = (String) params.get("fileName");
		String filePath = request.getSession().getServletContext().getRealPath("/") + "upload/";
		File file = new File(filePath + fileName);
		if(file.exists()) {
			params.put("exist", true);
			params.put("blob", FileUtils.readFileToByteArray(file));
		}else {
			params.put("exist", false);
		}
		return params;
	}
}

요청한 파라미터의 파일 데이터를 기준으로 존재하면 apache.commons 의 FileUtils 를 사용하여 File 데이터를 바이트로 변환했습니다.

 

request.js

function preview(file){
  $.ajax({
    type:"POST",
    url:"preView",
    data:fileName="+file,
    dataType: "json",
    success: function(args){
      if(args.exist){
      	$("#img").attr("src", "data:image/png;base64," + res.blob);
      }else{
      	alert("파일이 존재하지 않습니다.");
      }
    },
    error: function(error){
    	console.log(error);
    	alert("오류가 발생했습니다.");
    }
  });
}

서버 특정 경로에 존재하는 파일명.확장자 형태로 요청하면 서버에서 존재하는지 확인 후 byteArray 형태로 데이터를 전달해주면 base64를 통해 데이터를 이진데이터로 읽어들여서 이미지를 표출해준다. 다만 매번 이미지를 가져올때 마다 서버에서 통신이 발생하고 이진데이터로 노출하다보니 속도가 미세하게 느려지는게 느껴진다.

반응형
반응형

부트에서 스케줄러 사용하기

기존 Legacy 프로젝트를 구성할 때는 quartz를 통해  servlet.xml에 cron설정을 하여 주로 사용하여 왔는데, 부트에서는 추가 라이브러리 없이 쉽게 사용이 가능합니다.

 

 

기존 메인 설정은 그대로 사용하셔도 됩니다. 스케줄러 사용을 위해 어노테이션 @EnableScheduling를 추가합니다.

 

TestSpringBootApplication.java

@EnableScheduling  //스케줄링
@SpringBootApplication
public class TestSpringBootApplication {

	public static void main(String[] args) {
    		SpringApplication app = new SpringApplication(SrokSpringBootApplication.class);		
    		app.setBannerMode(Mode.CONSOLE);
    		app.run(args);
	}
}

설정을 하였으면, 스케줄러가 동작할 Class를 작성해보겠습니다.

 

 

 

SchedulerTest.java - cron버전

import java.time.LocalDateTime;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class SchedulerTest {
	
	/**
	 * cron탭을 사용한 스케줄러
	 */
	@Scheduled(cron="0 14 * * * ?")
	public void cronJobSch() {
		System.out.println("Sheduler time(cron) : " + LocalDateTime.now());
	}
}

cron 탭을 사용하여 동작 처리를 하였습니다.

예제의 cron은 매 14분 0초마다 동작하는 스케줄러입니다.

 

 

 

SchedulerTest.java - fixedDelay버전

@Component
public class SchedulerTest {

	/**
	 * 이전 수행 종료된 시간으로부터 입력 시간마다 호출됨
	 * 수행이 끝나고 1초뒤에 수행
	 */
	@Scheduled(fixedDelay = 1000)
	public void oneSecActionSch() {
		System.out.println("Sheduler time(fixedDelay) : " + LocalDateTime.now());
	}
}

이번엔 fixedDelay 속성으로 처리하였습니다. fixedDelay는 해당 스케줄러의 동작이 종료되고 입력한 n초뒤에 동작합니다. ex) 1000 = 1초

 

 

 

SchedulerTest.java - fixedRate버전

@Component
public class SchedulerTest {
	
	/**
	 * 수행이 시작된 시간으로부터 입력 시간마다 호출됨
	 * 수행이 시작되고 1초뒤 수행
	 */
	@Scheduled(fixedRate = 1000)
	public void rateActionSch() {
		System.out.println("Sheduler time(fixedRate) : " + LocalDateTime.now());
	}
}

마지막으로 fixedRate 방식입니다. fixedDelay와 비슷하지만 종료시간이 기준이 아닌 시작시간이 기준으로 해당 스케줄러가 동작하고 n초 뒤에 동작합니다.

 

 

 

반응형
반응형

SpringBoot 프로젝트를 war로 배포하고 명령어를 통해 프로젝트를 정상적으로 구동하였는데, 특정 페이지 이동시 해당 Exception 에러가 발생하는것을 확인 할 수 있었다.

 

jsp가 아닌 thymeleaf 템플릿을 적용하여 사용중이였는데, 이클립스에서 구동할때는 전혀 문제가 되지 않았지만, war로 배포후 구동시 절대경로 /preFix/view.html 형태로 적용하면 발생하는 에러로 html을 찾지 못해서 발생한 에러다.

 

컨트롤러에서 절대경로에서 상대경로 형태로 변경 후 정상 구동이 되었다.

 

view 경로를 변경하였다.

 

반응형
반응형

SpringBoot에서 프로젝트를 진행하던 도중 JUnit 테스트를 위해 Run As - JUnit Test 진행하였는데, 아래와 같은 에러가 발생하였다.

 

 

No tests found with test runner 'JUnit 5'

 

 

발생 원인으로 JUnit5의 설정을 처리한 소스를 구동하여서 발생한 원인으로 프로젝트의 사용하는 JUnit 버전을 확인해봐야한다. 필자는 JUnit4로 설정되어 있었다.

 

Properties - Java Build Path - Libraries - Classpath(JUnit4) - JUnit5로 변경 후 Apply and Close

 

1. 프로젝트 우클릭 후 Properties 메뉴에 접근한다.

 

2. Java Build Path - Libraries 항목에서 JUnit 버전을 5로 변경한다.

 

3. JUnit5로 변경하고 Apply and Close 후 구동하면 정상동작하는 것을 볼 수 있다.

반응형
반응형

application.properties

SpringBoot를 구동시 동작할 포트 정보, DB정보 등 여러가지 세팅 관련된 데이터나 별도의 경로 옵션등의 값을 프로퍼티에 넣어두고 사용됩니다.

기본 프로퍼티 위치는 /src/main/resources 아래에 존재하며 구동시 자동으로 감지되므로 별도로 프로퍼티의 위치를 명시적으로 등록하거나 경로를 입력할 필요가 없습니다.

 

 

프로퍼티를 외부에서 주입하려는 이유

부트 프로젝트를 빌드하여 배포하게 되면 추후 변경사항이 발생하여 변경하고자 할때 war안에 존재하다보니 명령어를 통해 교체해주거나 새로 빌드하여 배포해야 하는 번거로움이 존재합니다. 이러한 점을 보안하고자 애초에 배포시 프로퍼티 위치 자체를 war 밖으로 빼내면 설정파일을 열어서 간단하게 값을 변경 후 저장하고 부트 프로젝트만 구동시키면 되기에 외부에 존재하는 프로퍼티를 적용하는 방법을 찾아보았습니다.

 

 

프로퍼티 구동 순서

아래는 부트 구동시 프로퍼티를 찾는 순서입니다.

  1. 명령 줄 인수.
  2. Java System 속성 ( System.getProperties()).
  3. OS 환경 변수.
  4. @PropertySource@Configuration수업 에 대한 주석 .
  5. 패키지 된 jar 외부의 애플리케이션 속성 ( application.properties YAML 및 프로필 변형 포함).
  6. jar 내부에 패키지 된 애플리케이션 속성 ( application.properties YAML 및 프로필 변형 포함).
  7. 기본 속성 (을 사용하여 지정됨 SpringApplication.setDefaultProperties).

일반적으로 부트 구동시 사용되는 방법은 6번 내부에 패키지된 프로퍼티를 가져오는것입니다.

6번 구동방식

 

여기서 우리는 외부에 존재하는 프로퍼티 파일을 읽도록 처리해보겠습니다.

 

 

외부 프로퍼티 적용하기

먼저 테스트 환경 방식입니다.

- Spring Boot war 배포파일로 작성(내부 톰캣)

디렉토리 구성 및 파일 구성

 

정상적으로 property디렉토리 프로퍼티 파일을 읽으면 포트가 8100으로 구동됩니다.

 

1. 구동시 명령 옵션으로 처리하기

  java -jar app.war  

일반적인 부트 구동 명령어인데 여기서 옵션을 추가하여 property 디렉토리 내부의 application.properties를 로드하여 구동하겠습니다.

--spring.config.loaction 옵션을 통해 classpath에 존재하는 프로퍼티 또는 file을 통해 존재하는 파일을 명시해줄 수 있습니다.

--spring.config.name 옵션도 존재하는데, 해당 옵션을 통해 application.properties가 아닌 다른 프로퍼티를 읽도록 처리도 가능합니다.

 

적용하기 예제

절대경로를 통하여 file 위치를 명시하였습니다.

  java -jar app.war --spring.config.location=file:C:/test/property/application.properties  

정상적으로 8100으로 구동된 모습

 

2. jar 외부에 존재하는 프로퍼티를 읽기

사실 1번보다 간단한 방법인데 배포하는 디렉토리에 프로퍼티를 같이 위치하도록 하면 됩니다.

그럼 부트 구동 동작원리상 내부보다 외부를 먼저 보기때문에 자동으로 등록됩니다.

 

이번엔 명령줄에 별다른 옵션없이 구동하였지만 정상적으로 8100포트로 동작하는 모습을 볼 수 있습니다.

 

 

 

이 외에도 다른 설정 방법이 있지만 여기까지만 알아보도록 하겠습니다.

반응형
반응형

스프링 프로젝트를 구성하여 개발하다보면 사용자가 로그인이 되었는지 접근이 가능한 사용자인지 보안토큰이나 불필요한 파라미터나 파일등을 전송중인지 여러가지를 앞단에서 먼저 체크하고 통과시키거나 멈추게하는 기능을 처리 할 수 있는데, 필터 또는 인터셉터를 사용하여 처리가 가능합니다.

 

 

이 중에 컨트롤러에 도착하기전 먼저 계정의 세션정보나 접근레벨등을 처리하기 위한 인터셉터 적용법을 알아볼건데, 가로채다라는 뜻이 있듯이 컨트롤러에 도착하기전 데이터를 가로채서 요청한 데이터의 정보를 확인하여 해당 요청의 진행 유무를 판단할수있습니다.

 

 

먼저 스프링에서 요청이 발생했을때의 Life Cycle을 보시면 아래와 같습니다.

 

Filter와 Interceptor의 차이로 Dispatcher Servlet의 앞단에서 처리하냐, 뒷단에서 처리하냐의 차이가 존재합니다.

그림을 보시면 아시겠지만 Filter는 Servlet의 앞단에서 처리하며, Interceptor는 뒷단에서 처리합니다.

 

그럼 이제 적용방법과 사용법을 알아보겠습니다.

 

 

Interceptor 적용하기

구성환경

- STS4

- Java8

 

 

Interceptor 구성

먼저 interface HandlerInterceptor를 상속받아서 Interceptor를 구성합니다.

* sts4 기준으로 java8을 사용하며 HandlerInterceptor를 사용합니다. 기존에 상속받아서 사용하던 추상클래스HandlerInterceptorAdapter는 "Deprecated." 처리되었습니다.

public class AdminInterceptor implements HandlerInterceptor {
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		return true;
	}
	
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
	}
	
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object object, @Nullable Exception arg3) throws Exception {
	}
}

-preHandler : 컨트롤러에 도착하기전에 동작하는 메소드로 return값이 true이면 진행, false이면 멈춥니다.

-postHandler : 컨트롤러에 도착하여 view가 랜더링되기 전에 동작합니다.

-afterCompletion: view가 정상적으로 랜더링된 후에 마지막에 실행됩니다.

 

 

WebWebMvcConfigurer를 통한 Interceptor 설정

* sts4 java8 WebMvcConfigurer를 사용합니다. 기존에 상속받아서 사용하던 추상클래스 WebMvcConfigurerAdapter는 "Deprecated." 처리되었습니다.

@Configuration
public class ServerConfigure implements WebMvcConfigurer{
	private static final List<String> URL_PATTERNS = Arrays.asList("/async/*", "/board", "/user");  //인터셉터가 동작 해야 될 요청 주소 mapping 목록
	
	//인터셉터 주소 세팅
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new AdminInterceptor()).addPathPatterns(URL_PATTERNS);
	}
}

SpringLegacy에서는 servlet-context.xml에서 맵핑할 정보와 어떤 인터셉터를 적용할건지 처리를 하곤 했는데, 부트에서는 java를 통해 설정을 진행합니다.

 

addIntercepotrs를 통해 사용할 Interceptor를 등록하고. 패턴을 등록해줍니다.

-addPathPatterns : 해당 메소드는 동작해야할 url패턴을 설정합니다.

-excludePathPatterns: 해당 메소드는 적용한 인터셉터에서 제외할 url패턴을 설정합니다.

 

 

이상으로 springboot에서 Interceptor 적용법을 알아보았습니다.

 

반응형
반응형

myhappyman.tistory.com/172

 

Spring - 스프링 프로젝트에서 netty사용하기

기존에 운영중이던 프로젝트를 고도화 시키면서 특정 서비스단에서 전달된 값을 DB에 저장하고 저장된 정보를 바로 특정 소켓 서버로 전달을 해줘야 하는 기능을 작성해야 했습니다. 이미 tomcat

myhappyman.tistory.com

 

이전에 Controller부분에서 @PostConstruct 어노테이션을 활용하여 Thread를 생성하여 돌리고 서버를 따로 구동하는 형태로 작성하였는데, Bean등록을 하거나 @Component 어노테이션을 활용해서 등록하고 사용해보는 예제를 확인해보겠습니다.

Bean등록을 통한 설정

servlet-context.xml

<!-- Netty Server 등록 -->
<beans:bean id="nettyServer" class="package명.NettyServer" name="nettyServer"/>

정의한 NettyServer class파일을 등록하여 사용하면 됩니다.

 

@Component 어노테이션을 통한 설정

@Component
public class NettyServer {
    private static final Logger logger = Logger.getLogger(NettyServer.class);

    private final int SERVER_PORT = 15500;
    private final TestService testService;    

    @Autowired
    public NettyServer(TestService testService) {
        this.testService = testService;
    }

    private int SERVER_PORT;
    private ServerBootstrap sbs = new ServerBootstrap();
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;

    @PostConstruct
    public void run() {
        logger.info(" ============================= Netty Server Start ============================= ");
        bossGroup = new NioEventLoopGroup(20);
        workerGroup = new NioEventLoopGroup(20);

        sbs.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_TIMEOUT, 5 * 1000)
        .childOption(ChannelOption.SO_TIMEOUT, 5 * 1000)
        .childHandler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                // 응답없는 read상태 확인하는 Handler
                ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(5, 0, 0)); //아이들 상태

                // 메시지 송신, 수신, 응답없는 이벤트등을 처리할 Handler
                ch.pipeline().addLast("socketServerHandler", new NettyServerHandler(testService) ); //직접 동작 할 핸들러
            }
        });

        doConnect();
    }

    private void doConnect() {
        //서버는 Listen상태로 기다려야하는데, 톰캣이 Timeout이 발생함
        //이를 방지하기 위해 Thread로 처리한다.
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    ChannelFuture future = sbs.bind(SERVER_PORT).sync();
                    future.channel().closeFuture().sync();
                } catch (InterruptedException e) {
                    logger.error("InterruptedException", e);
                }
            }
        }).start();
    }

    @PreDestroy
    public void serverDestroy() throws InterruptedException {
        logger.info("================ Netty BootStrapServer Destroy() ================");
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
        sbs.group().shutdownGracefully();
    }
}

xml설정이 어렵다면 작성한 classBean으로 등록해주는 @Component 어노테이션을 활용하면 보다 쉽게 설정이 가능합니다.

 

추가해주시고 service, dao등 설정이 필요한 부분은 생성자를 통해 주입해줍니다.

 

이후 @PostConstruct 어노테이션을 활용하여 ServerBootStrap 옵션을 설정하고 구동을 해줍니다.

doConnect()함수에서 쓰레드를 생성하고 특정 포트로 바인딩하도록 설정하였는데, @PostConstruct는 해당 함수가 끝날때까지 대기하도록 설계되어 있는데, 해당부분에서 Netty 서버는 Listen상태로 계속 기다리고 있게됩니다.

톰캣입장에서는 구동 완료되는 시간이 Timeout설정이 되어 있는데 Timeout시간이 될때까지 설정이 끝나질 않으니 Timeout 에러가 발생하게 됩니다. 이부분을 막기위해 Thread를 통해 별도로 구성을 했습니다.

 

마무리

아직 스프링부트 프로젝트를 제대로 해볼 일이 없어서 아직도 Legacy만 쓰고 있는데, xml의 설정이 늘 어렵고 복잡하며 헷갈린것 같습니다. 이번 글에서 주의할 점은 xml에서 Bean등록을 했다면 Component 어노테이션은 지워야하고, Component 어노테이션으로 등록을 했다면 xml에서 Bean등록을 삭제해주시면 됩니다. 둘 다 설정하여도 동작은 하겠지만 2중으로 등록되면서 오류가 발생할 수 있습니다.

반응형
반응형

Spring에서 자주 바뀌는 값을 변수값으로 지정하여 개발하게 되면, 값이 변경이 필요한 경우 class파일 자체가 변경되어야 하기에 번거롭게 되는데, 이런 경우를 대비해 프로퍼티 파일을 읽게하여 재기동만으로 설정된 옵션(DB 연결IP, PORT 등)이나 정보등을 변경된 값으로 처리하여 수정 할 수 있습니다.

 

일반적으로 프로퍼티는 xml 설정을 통해 설정 하고 처리 할 수 있는데, ssh터널링과 같은 방식을 통해 DB를 연결하거나 해야 할때 xml설정으로 프로퍼티를 읽게되면 구동 순서상 에러가 발생할 수 있습니다.

 

이런 경우 xml에 Bean등록된 설정파일로 읽기가 아닌 자바에서 처리를 하시면 됩니다.

 

 

Java로 프로퍼티 파일 읽기

run()메소드에서 외부 메소드를 호출하여 읽고 정보별로 데이터를 얻는 예제입니다.

 

프로젝트 구조

 

 

config.properties

 

@Component
public class Test {
	private static final Logger logger = Logger.getLogger(Test.class);
	
	private final TestService testService;
	
	@Autowired
	public Test(TestService testService) {
		this.testService = testService;
	}

	@PostConstruct
	public void run() {
		
		GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
		ConfigurableEnvironment env = ctx.getEnvironment();
		MutablePropertySources prop = env.getPropertySources();
		try {
			prop.addLast(new ResourcePropertySource("classpath:config.properties"));
		} catch (IOException e) {
			logger.error("IOException", e);
		}
		
		// 프로퍼티 정보 얻기
		String ip = env.getProperty("db.ip");
		String pw = env.getProperty("db.pw");
		
		logger.info("IP : " + ip);
		logger.info("pw : " + pw);
	}
}

 

반응형