반응형

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

 

반응형
반응형

스프링 프로젝트에서 Netty 프레임워크를 사용하여 'ServerBootstrap'을 구동하고 사용하였는데, 톰캣을 종료하면 자연스럽게 죽을줄 알았던 Netty 서버가 좀비처럼 계속 살아있는 현상이 발생했다.

 

덕분에 톰캣을 재기동하게 되면 같은 포트로 바인딩하면서 BindException이 발생하는 등 여러가지 문제가 생겼고, 대처법으로 netstat을 통해 port의 pid를 찾아와 kill을 시킬까 했지만, 애초에 종료를 안하는 원초적인 문제를 해결하는것이 맞다고 판단하여 아래와 같이 '@PreDestroy' 어노테이션을 추가하여, 스프링을 종료하게되면 ServerBootstrp을 종료 할 수 있도록 구성하였다.

 

Netty Server 안전하게 종료하기

Controller.java

private NettyServer server;

@PostConstruct
private void start() {
	new Thread(new Runnable() {
		
		@Override
		public void run() {
			try {
				server = new NettyServer(SERVER_PORT, service);
				server.run();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
	}).start();
}

/**
 * 바인딩된 NettyServer를 종료한다.
 * @throws InterruptedException
 */
@PreDestroy
private void destory() throws InterruptedException {
	logger.info("================ Spring @PreDestroy ================");
	server.serverDestroy();
}

 

NettyServer.java

private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private ChannelFuture future;

public void serverDestroy() throws InterruptedException {
	logger.info("================ Netty BootStrapServer Destroy() ================");
	bossGroup.shutdownGracefully().sync();
	workerGroup.shutdownGracefully().sync();
	future.channel().closeFuture().sync();
}

구동하면서 동작시킨 이벤트 그룹들을 shutdown시켜서 종료되도록 처리하였다.

 

 

반응형
반응형

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

이미 tomcat으로 was서버를 운영중인데 소켓 서버를 운영하는건 여러가지로 문제가 있을것이라 판단하여 별도로 빼려고 했지만, 실시간성이 좀 더 중요시 되었고, 한 곳이 망가져도 바로 재부팅으로 올려도 괜찮다는 확인을 받아 바로 소켓처리를 진행해보았습니다.

 

먼저 단순 소켓서버만 올린다면 굳이 netty까진 쓰지 않아도 됩니다. io.Socket만으로도 충분히 훌륭하게 작성 할 수 있었지만 몇몇 요구사항들을 좀 더 쉽게 구현하기 위해 netty를 공부해보고 적용해보았던 소스를 공유하고자 합니다.

 

아래는 실제로 프로젝트에 적용해본 Socket Server, Socket Client 소스들입니다.

 

Netty Server

netty사용을 위해 메이븐 추가를 먼저 해줍니다.

 

pom.xml

<!-- netty -->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.0.33.Final</version>
</dependency>

먼저 Controller에@PostConstruct어노테이션을 활용하여 was 서비스 구동시netty가 동작하도록 별도의 쓰레드를 돌렸습니다.

 

NettyController.java

@Controller
public class NettyController {

    @PostConstruct
    private void start() {
        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    new NettySocketServer(5010).run();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

        }).start();
    }

    @PreDestroy
    private void destory() {

    }
}

서비스가 구동되면 start()가 동작되면서 별도로 작성한 NettySocketServer가 동작합니다.

내부에 5010은 임시로 지정한 소켓 포트입니다.

다음은 netty 소켓 서버를 확인해보겠습니다.

 

NettySocketServer.java

public class NettySocketServer {
    private int port;

    public NettySocketServer(int port) {
        this.port = port;
    }

    public void run() {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
         .channel(NioServerSocketChannel.class)
         .childHandler(new ChannelInitializer<SocketChannel>() {
             @Override
             public void initChannel(SocketChannel ch) throws Exception {
                 ChannelPipeline pipeline = ch.pipeline();
                 //handler setting
                 pipeline.addLast(new NettySocketServerHandler());
             }
         })
         .option(ChannelOption.SO_BACKLOG, 128)
         .childOption(ChannelOption.SO_KEEPALIVE, true);


        try {
            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

생성자를 통해 port값을 처리하고 run메소드를 통해 서버가 동작시 응답을 처리할 핸들러를 연결합니다.

다음은 핸들러입니다.

 

NettySocketServerHandler.java

@Sharable
public class NettySocketServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        String readMessage = ((ByteBuf) msg).toString(Charset.forName("UTF8"));
        ctx.write(msg);
        System.out.println("message from received: " + readMessage);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

@Sharable 어노테이션을 활용하여 안전하게 데이터를 처리하도록 변경하였습니다.

 

테스트결과

톰캣을 구동하고 서비스를 올립니다.

정상적으로 netty가 동작하는지 확인하기 위해 테스트툴로 5010포트를 통해 연결하고 메시지가 출력되는지 확인하겠습니다.

 

 

Netty Client

다음은 client를 구성해보겠습니다.

 

NettySocketClient.java

public class NettySocketClient {
	
	private String msg;
	private String host;
	private int port;
	
	public NettySocketClient(String host, int port, String msg){
		this.msg = msg;
		this.host = host;
		this.port = port;
	}
	
	public void run() {
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		
		try {
			Bootstrap b = new Bootstrap();
			b.group(workerGroup);
			b.channel(NioSocketChannel.class);
			b.option(ChannelOption.SO_KEEPALIVE, true);
			b.handler(new ChannelInitializer<SocketChannel>() {
	            @Override
	            public void initChannel(SocketChannel ch) throws Exception {
	                ch.pipeline().addLast(new NettySocketClientHandler(msg));
	            }
	        });
	        
	        //client connect
			try {
				ChannelFuture f = b.connect(host, port).sync();
				f.channel().closeFuture().sync();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}finally {
			workerGroup.shutdownGracefully();
		}
	}
}

생성자를 통해 연결할 host, ip, 발신할 message를 담는 문자열 msg값을 처리합니다.

 

해당 값들을 통해 run()메소드가 동작하면 연결정보로 연결이 됩니다.

다음으로 클라이언트 핸들러를 보겠습니다.

 

NettySocketClientHandler.java

public class NettySocketClientHandler extends ChannelInboundHandlerAdapter {

	private String msg;
	
	public NettySocketClientHandler(String msg) {
		this.msg = msg;
	}
	
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		ByteBuf messageBuffer = Unpooled.buffer();
		messageBuffer.writeBytes(msg.getBytes());
	
		ctx.writeAndFlush(messageBuffer);
	
		System.out.println("send message {" + msg + "}");
	}
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		System.out.println("receive message {" + ((ByteBuf) msg).toString(Charset.defaultCharset()) +"}");
	}
	
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
		ctx.close();
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		System.out.println(cause);
		ctx.close();
	}
}

 

클라이언트 테스트

서버툴에서 4564포트를 열어두고 연결하여 test msg send!!!라는 문구를 발송하였습니다.

캡처와 같이 서버에 정상적으로 메시지가 수신된 것을 볼 수 있습니다.

반응형
반응형

보통 파일 업로드를 할때 체크하는게 보통 파일의 마지막 '.'위치를 찾아서 확장자를 체크하고 파일의 사이즈정도를 항상 js를 통해 체크하는수준이였는데, 이번에 파일의 위·변조 체크요청이 있어서 찾다보니 파일의 MIME TYPE을 체크하여 위변조를 체크할 수 있는 방법을 확인하였습니다.

Apache Tika를 사용하여 파일의 MIME TYPE을 체크하며, 그전에 MIME TYPE에 대해 간단하게 알아보겠습니다.

 

MIME TYPE

Multipurpose Internet Mail Extensions의 약자로 인터넷에서 파일의 성격과 형식에 따라 파일을 식별하는 방법이다.

 

브라우저의 HTTP 응답에 정의된 Content-type의 헤더값을 확인하면 extension/plugin 이다.

과거 SMTP 프로토콜을 사용하여 전송되는 이메일을 위해 작성되었다.

 

하위 유형 앞에 x-로 시작하는 경우는 표준이 아니며 등록되지 않았다는것을 의미한다.

하위 유형 앞에 vnd가 붙으면 공급업체에 따라 변경된다는것을 의미한다.(msoffice 등)

 

출처: https://www.freeformatter.com/mime-types-list.html

 

zpage.mime.types.title

MIME Types List I have compiled a full list of MIME types using the "mime.types" file of the Apache HTTPD virtual private server. I took the liberty of adding a name/description for each MIME type so that it's clearer what they represent. I have also inclu

www.freeformatter.com

 

MIME TYPE은 이정도로 간단하게 알아보고 Tika를 사용하여 MIME TYPE을 알아보겠습니다.

 

 

Tika

pom.xml

<!-- Tika -->
<dependency>
    <groupId>org.apache.tika</groupId>
    <artifactId>tika-core</artifactId>
    <version>1.14</version>
</dependency>

dependency에 추가를 해줍니다.

 

 

(테스트 대상이 될 이미지 3개) 해당 파일들을 경로별로 체크해보겠습니다.

 

 

App.java

import java.io.File;
import java.io.IOException;

import org.apache.tika.Tika;

public class App{

    public static void main( String[] args ){
        File file1 = new File("C:/test/ggome.jpg");
        File file2 = new File("C:/test/kor.gif");
        File file3 = new File("C:/test/box.png");
        try {
            System.out.println(checkImageMimeType(file1));
            System.out.println(checkImageMimeType(file2));
            System.out.println(checkImageMimeType(file3));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static boolean checkImageMimeType(File file) throws IOException{
        Tika tika = new Tika();

        String mimeType = tika.detect(file);
        System.out.println(mimeType);
        if(mimeType.startsWith("image")) {
            return true;
        }else {
            return false;
        }
    }
}

각각 파일들을 detect메소드를 통해 처리해보니 각각 파일의 종류와 확장자를 sysout을 통해 찍어주는 것을 볼 수 있습니다.

 

 

위변조 체크해보기

자 그럼 위변조가 목적이었으니 애초에 텍스트였던 파일을 강제로 확장자를 변경해서 체크해보겠습니다.

이런식으로 텍스트 파일을 만들고 확장자를 변경합니다.

 

 

소스를 통해 확인해보겠습니다.

import java.io.File;
import java.io.IOException;

import org.apache.tika.Tika;

public class App{

    public static void main( String[] args ){
        File file1 = new File("C:/test/test.jpg");
        try {
            System.out.println(checkImageMimeType(file1));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static boolean checkImageMimeType(File file) throws IOException{
        Tika tika = new Tika();

        String mimeType = tika.detect(file);
        System.out.println(mimeType);
        if(mimeType.startsWith("image")) {
            return true;
        }else {
            return false;
        }
    }
}

확장자는 jpg이지만 텍스트 타입이라고 나오고 이미지타입이 아니기때문에 false처리된 것을 볼 수 있습니다.

반응형
반응형

모든 Controller(메소드)의 동작이 끝나고 로그를 처리하기 위해 SPRING AOP@after를 사용하여 기록을 하고 있었습니다.
작업 도중 파일 업로드를 위한 multipart의 타입의 전송의 경우 request의 값이 null값으로 처리되어 값을 받아 올 수 없었고 처리한 방법은 아래와 같이 2가지정도가 있었습니다.

  1. aop부분에서 형변환을 처리하여 값을 변경하였습니다.

  2. Interceptor에서 request의 타입을 체크하여 RequestContextHolder의 속성값을 변경하였습니다.

 

after어노테이션에서 형변환 방식

@After("execution(public * egovframework.*.*.*.*(..))")
public void logWrite(JoinPoint joinPoint) throws Throwable {
    final String methodName = joinPoint.getSignature().getName();
    ServletRequestAttributes attribute = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes());
    HttpServletRequest request = attribute.getRequest();

    // 멀티 파트 체크 **************
    if (request.getContentType() != null && request.getContentType().toLowerCase().contains("multipart/form-data")) {
        MultiReadAndCopyHttpServletRequest copyReq = new MultiReadAndCopyHttpServletRequest(request); //Request복사
        CommonsMultipartResolver cmResolver = new CommonsMultipartResolver();  //형식변환준비
        MultipartHttpServletRequest req = cmResolver.resolveMultipart(copyReq); //변환
        request = req;
    }
    // 멀티 파트 체크 **************

    (...) //이후 로그처리

첫번째 해결방식으로 reqeust의 값이 null인지 체크후 multipart/form-data인지 확인 후 일치한다면, requestMultipartHttpServletReqeust로 형변환처리하였습니다.

해당 방법으로 해결은 가능하지만 AOP의 양이 많아지거나 처리로직이 많아지면 가독성면이나 이후 유지보수 측면에서 좋지 않을것으로 판단하여 다른방법을 찾아보았고 Interceptor에서 처리하는 방법을 확인하였습니다.

 

 

Interceptor 추가하기

두번째 처리방법으로 Interceptor를 추가하고 instanceof를 통해 타입을 체크 후 변경해주도록 하였습니다.

 

MultipartHandlerInterceptor.java

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

/**
 * form태그 encType이 "multi/part"로 넘어갈경우 request set변경처리
 * @author srok
 *
 */
public class MultipartHandlerInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (request instanceof MultipartHttpServletRequest) {
            RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
        }
        return true;
    }
}

 

 

dispatcher-servlet.xml

Interceptor 정보 추가

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/*.do" />
        <bean class="프로젝트 경로.MultipartHandlerInterceptor" />
    </mvc:interceptor>
</mvc:interceptors>

실 적용은 두번째 방식을 적용하였습니다.

 

 

 

두번째 처리방식 참조 URL

참조 : https://hhjeong.tistory.com/74

반응형
반응형

스프링 구조에서 3버전이 너무 낮아 4버전으로 올려서 프로젝트를 진행하던 중

java.lang.IllegalArgumentException: No converter found for return value of type: class java.util.ArrayList

 

같은 에러를 만나게 되었다.

 

컨트롤러에서 JSON형태로 LIST데이터를 리턴하면서 발생하는 에러로 아래와 같이 gson 라이브러리를 추가하여 해결하였다.

 

pom.xml

<dependency>
	<groupId>com.google.code.gson</groupId>
	<artifactId>gson</artifactId>
	<version>2.8.5</version>
</dependency>
반응형
반응형

필터에서 DB에서 받아온 정보를 사용하고 싶은 경우가 발생하였다.

 

초기화 함수인 init에서 서비스단이나 DAO를 사용하면 아직 bean등록이 되지 않은 상태로 초기화함수를 사용할 수 없었고 @PostConstruct를 활용하여 데이터를 처리할 수 있었다.

 

어찌저찌 처리는 하였지만 sysout을 통해 찍어보니 로그가 3번씩 찍히는 현상을 발견하였다.

 

구조부터 spring문서나 2번이상 발생되는 자료를 찾아봤지만 딱히 찾을 수 없었는데, 원인은 소스의 설정방법에 문제가 있었다.

 

해당 필터를 빈등록을 하기 위해 @Component 어노테이션을 설정하였다.

여기서 문제는 web.xml에 필터태그로 필터를 추가 등록하면서 bean등록이 2번 되었기 때문이다.

 

문제가 된 소스부분

@component 설정
web.xml에 filter 설정

 

둘 중 한개의 설정을 제거하고 한번만 정상적으로 동작하는것을 확인 하였다.

반응형